@nitra/cursor 1.8.103 → 1.8.105
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/auto-rules.md +45 -0
- package/bin/n-cursor.js +280 -149
- package/mdc/graphql.mdc +15 -1
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +54 -36
- package/scripts/check-bun.mjs +2 -6
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +15 -11
- package/scripts/check-k8s.mjs +1652 -627
- package/scripts/check-nginx-default-tpl.mjs +17 -10
- package/scripts/check-npm-module.mjs +3 -3
- package/scripts/check-text.mjs +1 -3
- package/scripts/check-vue.mjs +2 -2
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +90 -72
- package/scripts/utils/workspaces.mjs +39 -16
package/scripts/check-abie.mjs
CHANGED
|
@@ -64,6 +64,28 @@ const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NA
|
|
|
64
64
|
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
65
65
|
|
|
66
66
|
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
67
|
+
const LINE_SPLIT_RE = /\r?\n/u
|
|
68
|
+
const RU_KUSTOMIZATION_PATH_RE = /(^|\/)ru\/kustomization\.yaml$/u
|
|
69
|
+
const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
|
|
70
|
+
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u
|
|
71
|
+
const BASE_SEGMENT_RE = /(^|\/)base\//u
|
|
72
|
+
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
73
|
+
const K8S_PACKAGE_DIR_RE = /^(.+)\/k8s\//u
|
|
74
|
+
const PATCH_PATH_TYPE_RE = /path:\s*\/spec\/type\b/u
|
|
75
|
+
const PATCH_VALUE_NODE_PORT_RE = /value:\s*['"]?NodePort['"]?(?:\s|$)/iu
|
|
76
|
+
const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
|
|
77
|
+
const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
78
|
+
const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
|
|
79
|
+
const TRAILING_SLASH_RE = /\/$/u
|
|
80
|
+
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
81
|
+
const PATCH_PARENT_REF_NS_UA_RE = /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu
|
|
82
|
+
const PATCH_PARENT_REF_NS_RU_RE = /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu
|
|
83
|
+
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
84
|
+
const LEADING_EMPTY_LINE_RE = /^\s*\n/u
|
|
85
|
+
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
86
|
+
const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
87
|
+
const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
|
|
88
|
+
const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
67
89
|
|
|
68
90
|
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
69
91
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
@@ -75,7 +97,7 @@ export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
|
75
97
|
*/
|
|
76
98
|
export function isRuKustomizationPath(rel) {
|
|
77
99
|
const norm = rel.replaceAll('\\', '/')
|
|
78
|
-
return
|
|
100
|
+
return RU_KUSTOMIZATION_PATH_RE.test(norm)
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
|
@@ -85,7 +107,7 @@ export function isRuKustomizationPath(rel) {
|
|
|
85
107
|
*/
|
|
86
108
|
export function isUaKustomizationPath(rel) {
|
|
87
109
|
const norm = rel.replaceAll('\\', '/')
|
|
88
|
-
return
|
|
110
|
+
return UA_KUSTOMIZATION_PATH_RE.test(norm)
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
/**
|
|
@@ -96,7 +118,7 @@ export function isUaKustomizationPath(rel) {
|
|
|
96
118
|
*/
|
|
97
119
|
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
98
120
|
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
99
|
-
const m = rel.match(
|
|
121
|
+
const m = rel.match(OVERLAY_PACKAGE_DIR_RE)
|
|
100
122
|
return m ? join(root, m[1]) : null
|
|
101
123
|
}
|
|
102
124
|
|
|
@@ -147,7 +169,7 @@ export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizat
|
|
|
147
169
|
*/
|
|
148
170
|
export function isAbieK8sBaseYamlPath(rel) {
|
|
149
171
|
const norm = rel.replaceAll('\\', '/')
|
|
150
|
-
return
|
|
172
|
+
return BASE_SEGMENT_RE.test(norm)
|
|
151
173
|
}
|
|
152
174
|
|
|
153
175
|
/**
|
|
@@ -276,7 +298,7 @@ async function findK8sYamlFiles(root) {
|
|
|
276
298
|
if (!pathHasK8sSegment(p)) {
|
|
277
299
|
return
|
|
278
300
|
}
|
|
279
|
-
if (
|
|
301
|
+
if (!YAML_EXTENSION_RE.test(p)) {
|
|
280
302
|
return
|
|
281
303
|
}
|
|
282
304
|
out.push(p)
|
|
@@ -335,7 +357,7 @@ function k8sYamlRelOutsideUaRuOverlays(relFromRoot) {
|
|
|
335
357
|
*/
|
|
336
358
|
function abiePackageDirFromK8sYamlRel(root, relFromRoot) {
|
|
337
359
|
const norm = relFromRoot.replaceAll('\\', '/')
|
|
338
|
-
const m = norm.match(
|
|
360
|
+
const m = norm.match(K8S_PACKAGE_DIR_RE)
|
|
339
361
|
return m ? join(root, m[1]) : null
|
|
340
362
|
}
|
|
341
363
|
|
|
@@ -418,12 +440,10 @@ export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
|
418
440
|
if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
|
|
419
441
|
return false
|
|
420
442
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const opRe = String.raw`op:\s*remove\b`
|
|
426
|
-
return new RegExp(String.raw`${opRe}[\s\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(String.raw`${pathRe}[\s\S]{0,200}?${opRe}`, 'mu').test(patchText)
|
|
443
|
+
if (posixPath === '/spec/clusterIP') {
|
|
444
|
+
return REMOVE_CLUSTER_IP_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IP_BEFORE_OP_RE.test(patchText)
|
|
445
|
+
}
|
|
446
|
+
return REMOVE_CLUSTER_IPS_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IPS_BEFORE_OP_RE.test(patchText)
|
|
427
447
|
}
|
|
428
448
|
|
|
429
449
|
/**
|
|
@@ -444,10 +464,10 @@ export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
|
|
|
444
464
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
445
465
|
return false
|
|
446
466
|
}
|
|
447
|
-
if (
|
|
467
|
+
if (!PATCH_PATH_TYPE_RE.test(patchText)) {
|
|
448
468
|
return false
|
|
449
469
|
}
|
|
450
|
-
if (
|
|
470
|
+
if (!PATCH_VALUE_NODE_PORT_RE.test(patchText)) {
|
|
451
471
|
return false
|
|
452
472
|
}
|
|
453
473
|
return true
|
|
@@ -502,7 +522,7 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
|
502
522
|
*/
|
|
503
523
|
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
504
524
|
const body = stripBom(raw)
|
|
505
|
-
const lines = body.split(
|
|
525
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
506
526
|
const first = lines[0] ?? ''
|
|
507
527
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
508
528
|
/** @type {Map<string, string>} */
|
|
@@ -589,7 +609,7 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
|
|
|
589
609
|
}
|
|
590
610
|
if (readOk) {
|
|
591
611
|
const body = stripBom(raw)
|
|
592
|
-
const lines = body.split(
|
|
612
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
593
613
|
const first = lines[0] ?? ''
|
|
594
614
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
595
615
|
/** @type {import('yaml').Document[]} */
|
|
@@ -620,8 +640,8 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
|
|
|
620
640
|
const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
|
|
621
641
|
const prev = inner.get(n)
|
|
622
642
|
inner.set(n, {
|
|
623
|
-
requiresClusterIPNoneClear:
|
|
624
|
-
requiresClusterIPsRemove:
|
|
643
|
+
requiresClusterIPNoneClear: prev?.requiresClusterIPNoneClear === true || needClear,
|
|
644
|
+
requiresClusterIPsRemove: prev?.requiresClusterIPsRemove === true || needClusterIPsRemove
|
|
625
645
|
})
|
|
626
646
|
}
|
|
627
647
|
}
|
|
@@ -700,7 +720,7 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
700
720
|
}
|
|
701
721
|
if (readOk) {
|
|
702
722
|
const body = stripBom(raw)
|
|
703
|
-
const lines = body.split(
|
|
723
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
704
724
|
const first = lines[0] ?? ''
|
|
705
725
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
706
726
|
/** @type {import('yaml').Document[]} */
|
|
@@ -753,7 +773,7 @@ async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fai
|
|
|
753
773
|
return
|
|
754
774
|
}
|
|
755
775
|
const body = stripBom(raw)
|
|
756
|
-
const lines = body.split(
|
|
776
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
757
777
|
const first = lines[0] ?? ''
|
|
758
778
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
759
779
|
/** @type {import('yaml').Document[]} */
|
|
@@ -806,10 +826,10 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
806
826
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
807
827
|
return false
|
|
808
828
|
}
|
|
809
|
-
if (
|
|
829
|
+
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
810
830
|
return false
|
|
811
831
|
}
|
|
812
|
-
if (
|
|
832
|
+
if (!PATCH_PREEM_FALSE_RE.test(patchText)) {
|
|
813
833
|
return false
|
|
814
834
|
}
|
|
815
835
|
return true
|
|
@@ -825,10 +845,10 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
|
825
845
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
826
846
|
return false
|
|
827
847
|
}
|
|
828
|
-
if (
|
|
848
|
+
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
829
849
|
return false
|
|
830
850
|
}
|
|
831
|
-
if (
|
|
851
|
+
if (!PATCH_YANDEX_PREEMPTIBLE_FALSE_RE.test(patchText)) {
|
|
832
852
|
return false
|
|
833
853
|
}
|
|
834
854
|
return true
|
|
@@ -904,7 +924,7 @@ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
|
904
924
|
*/
|
|
905
925
|
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
906
926
|
const body = stripBom(raw)
|
|
907
|
-
const lines = body.split(
|
|
927
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
908
928
|
const first = lines[0] ?? ''
|
|
909
929
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
910
930
|
/** @type {import('yaml').Document[]} */
|
|
@@ -930,7 +950,7 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
930
950
|
*/
|
|
931
951
|
export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
|
|
932
952
|
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
933
|
-
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(
|
|
953
|
+
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
|
|
934
954
|
const prefix = `${pkg}/k8s/`
|
|
935
955
|
if (!normRel.startsWith(prefix)) {
|
|
936
956
|
return false
|
|
@@ -1010,7 +1030,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1010
1030
|
}
|
|
1011
1031
|
if (raw !== undefined) {
|
|
1012
1032
|
const body = stripBom(raw)
|
|
1013
|
-
const lines = body.split(
|
|
1033
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1014
1034
|
const first = lines[0] ?? ''
|
|
1015
1035
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1016
1036
|
/** @type {import('yaml').Document[] | undefined} */
|
|
@@ -1109,7 +1129,7 @@ function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
|
1109
1129
|
*/
|
|
1110
1130
|
export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
1111
1131
|
const body = stripBom(raw)
|
|
1112
|
-
const lines = body.split(
|
|
1132
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1113
1133
|
const first = lines[0] ?? ''
|
|
1114
1134
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1115
1135
|
/** @type {import('yaml').Document[]} */
|
|
@@ -1144,7 +1164,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1144
1164
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
1145
1165
|
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
|
|
1146
1166
|
}
|
|
1147
|
-
if (
|
|
1167
|
+
if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
|
|
1148
1168
|
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
1149
1169
|
}
|
|
1150
1170
|
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
@@ -1152,9 +1172,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1152
1172
|
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
1153
1173
|
}
|
|
1154
1174
|
const namespaceOk =
|
|
1155
|
-
mode === 'ua'
|
|
1156
|
-
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
1157
|
-
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
1175
|
+
mode === 'ua' ? PATCH_PARENT_REF_NS_UA_RE.test(combined) : PATCH_PARENT_REF_NS_RU_RE.test(combined)
|
|
1158
1176
|
if (!namespaceOk) {
|
|
1159
1177
|
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
1160
1178
|
}
|
|
@@ -1162,7 +1180,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1162
1180
|
mode === 'ru' &&
|
|
1163
1181
|
typeof fullKustomizationRaw === 'string' &&
|
|
1164
1182
|
fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
|
|
1165
|
-
if (ruNeedsWebsocket &&
|
|
1183
|
+
if (ruNeedsWebsocket && !WEBSOCKET_ANNOTATION_RE.test(combined)) {
|
|
1166
1184
|
return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
1167
1185
|
}
|
|
1168
1186
|
const sharedCount =
|
|
@@ -1197,7 +1215,7 @@ export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
|
1197
1215
|
*/
|
|
1198
1216
|
export function validateAbieHcYaml(raw, relPath) {
|
|
1199
1217
|
const body = stripBom(raw)
|
|
1200
|
-
const lines = body.split(
|
|
1218
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1201
1219
|
if (lines.length === 0 || lines[0].trim() === '') {
|
|
1202
1220
|
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
1203
1221
|
}
|
|
@@ -1211,7 +1229,7 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1211
1229
|
const yamlBody = lines
|
|
1212
1230
|
.slice(1)
|
|
1213
1231
|
.join('\n')
|
|
1214
|
-
.replace(
|
|
1232
|
+
.replace(LEADING_EMPTY_LINE_RE, '')
|
|
1215
1233
|
/** @type {import('yaml').Document[]} */
|
|
1216
1234
|
let docs
|
|
1217
1235
|
try {
|
|
@@ -1308,7 +1326,7 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
|
1308
1326
|
}
|
|
1309
1327
|
if (raw !== null) {
|
|
1310
1328
|
const body = stripBom(raw)
|
|
1311
|
-
const lines = body.split(
|
|
1329
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1312
1330
|
const first = lines[0] ?? ''
|
|
1313
1331
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1314
1332
|
try {
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -106,9 +106,7 @@ export async function check() {
|
|
|
106
106
|
} else {
|
|
107
107
|
const bad = Object.keys(dev).filter(n => !isAllowedRootDevDependency(n))
|
|
108
108
|
if (bad.length > 0) {
|
|
109
|
-
fail(
|
|
110
|
-
`Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`
|
|
111
|
-
)
|
|
109
|
+
fail(`Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`)
|
|
112
110
|
} else {
|
|
113
111
|
const n = Object.keys(dev).length
|
|
114
112
|
pass(
|
|
@@ -145,9 +143,7 @@ export async function check() {
|
|
|
145
143
|
const missing = lintPrefixed.filter(name => !aggregate.includes(`bun run ${name}`))
|
|
146
144
|
if (missing.length > 0) {
|
|
147
145
|
const missingList = missing.map(s => `\`${s}\``).join(', ')
|
|
148
|
-
fail(
|
|
149
|
-
`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missingList}`
|
|
150
|
-
)
|
|
146
|
+
fail(`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missingList}`)
|
|
151
147
|
} else {
|
|
152
148
|
pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
|
|
153
149
|
if (OXFMT_END_RE.test(aggregate.trim())) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml
|
|
2
|
+
* Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`**, рекомендації
|
|
3
|
+
* **`graphql.vscode-graphql`** і скрипта **`dump-schema`** у кореневому
|
|
4
|
+
* **`package.json`**, якщо у дереві є **`gql\`…\``**.
|
|
3
5
|
*
|
|
4
6
|
* Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; пропуск **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
|
|
5
7
|
*
|
|
@@ -22,16 +24,16 @@ export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
|
|
|
22
24
|
|
|
23
25
|
/** Розширення VS Code з graphql.mdc. */
|
|
24
26
|
export const REQUIRED_GRAPHQL_VSCODE_EXTENSION = 'graphql.vscode-graphql'
|
|
27
|
+
/** Команда dump-schema з graphql.mdc. */
|
|
28
|
+
export const REQUIRED_DUMP_SCHEMA_SCRIPT =
|
|
29
|
+
"bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql"
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @
|
|
32
|
+
* Збирає абсолютні шляхи source-файлів, які підлягають скануванню на gql templates.
|
|
33
|
+
* @param {string} root абсолютний шлях кореня
|
|
34
|
+
* @returns {Promise<string[]>} список кандидатів
|
|
29
35
|
*/
|
|
30
|
-
|
|
31
|
-
const reporter = createCheckReporter()
|
|
32
|
-
const { pass, fail } = reporter
|
|
33
|
-
|
|
34
|
-
const root = process.cwd()
|
|
36
|
+
async function collectScanCandidates(root) {
|
|
35
37
|
/** @type {string[]} */
|
|
36
38
|
const candidates = []
|
|
37
39
|
await walkDir(root, absPath => {
|
|
@@ -41,7 +43,16 @@ export async function check() {
|
|
|
41
43
|
}
|
|
42
44
|
candidates.push(absPath)
|
|
43
45
|
})
|
|
46
|
+
return candidates
|
|
47
|
+
}
|
|
44
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Повертає відносні шляхи файлів, де знайдено gql tagged template.
|
|
51
|
+
* @param {string} root абсолютний шлях кореня
|
|
52
|
+
* @param {string[]} candidates абсолютні шляхи файлів-кандидатів
|
|
53
|
+
* @returns {Promise<string[]>} відносні шляхи файлів зі збігами
|
|
54
|
+
*/
|
|
55
|
+
async function collectGqlHits(root, candidates) {
|
|
45
56
|
/** @type {string[]} */
|
|
46
57
|
const hits = []
|
|
47
58
|
for (const absPath of candidates) {
|
|
@@ -51,9 +62,99 @@ export async function check() {
|
|
|
51
62
|
hits.push(rel)
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
return hits
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Перевіряє `.vscode/extensions.json` на рекомендацію GraphQL extension.
|
|
70
|
+
* @param {(msg: string) => void} pass success-репортер
|
|
71
|
+
* @param {(msg: string) => void} fail fail-репортер
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async function checkExtensionsRecommendation(pass, fail) {
|
|
75
|
+
if (!existsSync('.vscode/extensions.json')) {
|
|
76
|
+
fail(
|
|
77
|
+
'.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let ext
|
|
83
|
+
try {
|
|
84
|
+
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
85
|
+
} catch {
|
|
86
|
+
fail('.vscode/extensions.json не є валідним JSON')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rec = ext.recommendations
|
|
91
|
+
if (!Array.isArray(rec)) {
|
|
92
|
+
fail('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (rec.includes(REQUIRED_GRAPHQL_VSCODE_EXTENSION)) {
|
|
97
|
+
pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
|
|
98
|
+
} else {
|
|
99
|
+
fail(`.vscode/extensions.json: додай у recommendations "${REQUIRED_GRAPHQL_VSCODE_EXTENSION}" (graphql.mdc)`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Перевіряє `package.json` і значення scripts.dump-schema.
|
|
105
|
+
* @param {(msg: string) => void} pass success-репортер
|
|
106
|
+
* @param {(msg: string) => void} fail fail-репортер
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
async function checkPackageDumpSchemaScript(pass, fail) {
|
|
110
|
+
if (!existsSync('package.json')) {
|
|
111
|
+
fail('Відсутній package.json у корені репозиторію')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let pkg
|
|
116
|
+
try {
|
|
117
|
+
pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
118
|
+
} catch {
|
|
119
|
+
fail('package.json не є валідним JSON')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const scripts = pkg.scripts
|
|
124
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
125
|
+
fail('package.json: поле scripts має бути обʼєктом')
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Object.hasOwn(scripts, 'dump-schema')) {
|
|
130
|
+
fail('package.json: відсутній scripts.dump-schema (graphql.mdc)')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (scripts['dump-schema'] === REQUIRED_DUMP_SCHEMA_SCRIPT) {
|
|
135
|
+
pass('package.json: scripts.dump-schema відповідає graphql.mdc')
|
|
136
|
+
} else {
|
|
137
|
+
fail(`package.json: scripts.dump-schema має бути "${REQUIRED_DUMP_SCHEMA_SCRIPT}" (graphql.mdc)`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Перевіряє graphql.mdc: умовна вимога .graphqlrc.yml, graphql.vscode-graphql
|
|
143
|
+
* і scripts.dump-schema за наявності gql tagged templates.
|
|
144
|
+
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
145
|
+
*/
|
|
146
|
+
export async function check() {
|
|
147
|
+
const reporter = createCheckReporter()
|
|
148
|
+
const { pass, fail } = reporter
|
|
149
|
+
|
|
150
|
+
const root = process.cwd()
|
|
151
|
+
const candidates = await collectScanCandidates(root)
|
|
152
|
+
const hits = await collectGqlHits(root, candidates)
|
|
54
153
|
|
|
55
154
|
if (hits.length === 0) {
|
|
56
|
-
pass(
|
|
155
|
+
pass(
|
|
156
|
+
`Немає tagged template з тегом gql у .vue / JS / TS джерелах (переглянуто ${candidates.length} файлів) — .graphqlrc.yml не вимагається`
|
|
157
|
+
)
|
|
57
158
|
return reporter.getExitCode()
|
|
58
159
|
}
|
|
59
160
|
|
|
@@ -67,31 +168,8 @@ export async function check() {
|
|
|
67
168
|
)
|
|
68
169
|
}
|
|
69
170
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
74
|
-
} catch {
|
|
75
|
-
fail('.vscode/extensions.json не є валідним JSON')
|
|
76
|
-
ext = null
|
|
77
|
-
}
|
|
78
|
-
if (ext) {
|
|
79
|
-
const rec = ext.recommendations
|
|
80
|
-
if (!Array.isArray(rec)) {
|
|
81
|
-
fail('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
82
|
-
} else if (rec.includes(REQUIRED_GRAPHQL_VSCODE_EXTENSION)) {
|
|
83
|
-
pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
|
|
84
|
-
} else {
|
|
85
|
-
fail(
|
|
86
|
-
`.vscode/extensions.json: додай у recommendations "${REQUIRED_GRAPHQL_VSCODE_EXTENSION}" (graphql.mdc)`
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
fail(
|
|
92
|
-
'.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
|
|
93
|
-
)
|
|
94
|
-
}
|
|
171
|
+
await checkExtensionsRecommendation(pass, fail)
|
|
172
|
+
await checkPackageDumpSchemaScript(pass, fail)
|
|
95
173
|
|
|
96
174
|
return reporter.getExitCode()
|
|
97
175
|
}
|
|
@@ -19,13 +19,17 @@ export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bu
|
|
|
19
19
|
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
20
20
|
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
21
21
|
|
|
22
|
+
const WHITESPACE_RE = /\s+/gu
|
|
23
|
+
const NON_DIGITS_RE = /\D+/u
|
|
24
|
+
const OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Нормалізує рядок скрипта для порівняння (зайві пробіли).
|
|
24
28
|
* @param {string} s вихідний рядок скрипта `lint-js`
|
|
25
29
|
* @returns {string} рядок без зайвих пробілів на краях і з одиничними пробілами всередині
|
|
26
30
|
*/
|
|
27
31
|
export function normalizeLintJsScript(s) {
|
|
28
|
-
return String(s).trim().replaceAll(
|
|
32
|
+
return String(s).trim().replaceAll(WHITESPACE_RE, ' ')
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -47,13 +51,14 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
|
|
|
47
51
|
if (s.startsWith('workspace:')) {
|
|
48
52
|
return true
|
|
49
53
|
}
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
54
|
+
const parts = s.split(NON_DIGITS_RE).filter(Boolean)
|
|
55
|
+
if (parts.length < 3) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
const [major, minor, patch] = parts.slice(0, 3).map(Number)
|
|
59
|
+
if ([major, minor, patch].some(n => Number.isNaN(n))) {
|
|
52
60
|
return false
|
|
53
61
|
}
|
|
54
|
-
const major = Number(m[1])
|
|
55
|
-
const minor = Number(m[2])
|
|
56
|
-
const patch = Number(m[3])
|
|
57
62
|
return major > 3 || (major === 3 && minor > 5) || (major === 3 && minor === 5 && patch >= 0)
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -167,8 +172,8 @@ export async function check() {
|
|
|
167
172
|
|
|
168
173
|
const nodeEngine = pkg.engines?.node
|
|
169
174
|
if (nodeEngine) {
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
175
|
+
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
176
|
+
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
172
177
|
pass(`engines.node: "${nodeEngine}"`)
|
|
173
178
|
} else {
|
|
174
179
|
fail(`engines.node: "${nodeEngine}" — має бути >=24`)
|
|
@@ -255,7 +260,7 @@ export async function check() {
|
|
|
255
260
|
fail(errMsg)
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
|
-
if (content.includes('bunx oxlint') &&
|
|
263
|
+
if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
|
|
259
264
|
fail('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
260
265
|
}
|
|
261
266
|
if (content.includes('eslint --fix')) {
|
|
@@ -268,8 +273,7 @@ export async function check() {
|
|
|
268
273
|
|
|
269
274
|
if (existsSync('.github/workflows/lint.yml')) {
|
|
270
275
|
const lintYml = await readFile('.github/workflows/lint.yml', 'utf8')
|
|
271
|
-
const looksLikeJsLint =
|
|
272
|
-
/\bbunx\s+oxlint\b/u.test(lintYml) && /\bbunx\s+eslint\b/u.test(lintYml) && /\bjscpd\b/u.test(lintYml)
|
|
276
|
+
const looksLikeJsLint = lintYml.includes('bunx oxlint') && lintYml.includes('bunx eslint') && lintYml.includes('jscpd')
|
|
273
277
|
if (looksLikeJsLint) {
|
|
274
278
|
fail('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js-lint.mdc)')
|
|
275
279
|
} else {
|