@nitra/cursor 1.8.207 → 1.8.209
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/CHANGELOG.md +91 -0
- package/mdc/js-run.mdc +49 -2
- package/package.json +1 -1
- package/policy/abie/health_check_policy/health_check_policy.rego +5 -1
- package/policy/abie/http_route_base/http_route_base.rego +2 -1
- package/policy/hasura/svc_hl/svc_hl.rego +2 -1
- package/policy/k8s/manifest/manifest.rego +2 -0
- package/scripts/check-adr.mjs +10 -88
- package/scripts/check-ga.mjs +14 -192
- package/scripts/check-js-lint.mjs +14 -115
- package/scripts/check-js-run.mjs +81 -83
- package/scripts/check-npm-module.mjs +17 -155
- package/scripts/utils/conn-file-rules.mjs +170 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевірки для файлів-підключень у каталозі `#conn` (js-run.mdc → «Нейминг файлів у `src/conn/`»
|
|
3
|
+
* та «Експорти у файлах `src/conn/`»).
|
|
4
|
+
*
|
|
5
|
+
* Канонічна назва файла:
|
|
6
|
+
* - GraphQL: `ql-<id>.{js|mjs|cjs|ts|mts|cts}` (id — kebab-case ідентифікатор endpoint);
|
|
7
|
+
* - PostgreSQL: `pg-{read|write}.{ext}` або `pg-{read|write}-<id>.{ext}` (id — для multi-БД);
|
|
8
|
+
* - MySQL/MSSQL: `mysql-{read|write}.{ext}` або `mysql-{read|write}-<id>.{ext}`.
|
|
9
|
+
*
|
|
10
|
+
* Канонічний експорт — іменований, без `export default`. Імʼя константи має дорівнювати
|
|
11
|
+
* camelCase від basename файла (`pg-write-contract` → `pgWriteContract`).
|
|
12
|
+
*
|
|
13
|
+
* Парсимо через oxc-parser; коли файл не парситься — повертаємо порожні результати, щоб
|
|
14
|
+
* не змішувати помилки синтаксису з порушеннями цього правила.
|
|
15
|
+
*/
|
|
16
|
+
import { parseProgramOrNull } from './ast-scan-utils.mjs'
|
|
17
|
+
|
|
18
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Канонічний шаблон імені файла в каталозі conn.
|
|
22
|
+
* - `ql-<id>` для GraphQL;
|
|
23
|
+
* - `(pg|mysql)-(read|write)(-<id>)?` для БД.
|
|
24
|
+
* `<id>` — починається з [a-z0-9], далі [a-z0-9-]*.
|
|
25
|
+
*/
|
|
26
|
+
const CONN_FILENAME_RE =
|
|
27
|
+
/^(?:ql-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|(?:pg|mysql)-(?:read|write)(?:-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)?)\.([cm]?[jt]sx?)$/u
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Чи це файл, який сканується правилом «conn-file» (JS/TS-сімʼя, без `.d.ts`).
|
|
31
|
+
* @param {string} relativePathPosix відносний posix-шлях
|
|
32
|
+
* @returns {boolean} true, якщо потрібно перевіряти
|
|
33
|
+
*/
|
|
34
|
+
export function isConnFileRulesSourceFile(relativePathPosix) {
|
|
35
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Витягує basename файла без розширення.
|
|
40
|
+
* @param {string} relativePathPosix відносний шлях у posix-форматі
|
|
41
|
+
* @returns {string} basename без розширення (наприклад, `pg-write-contract`)
|
|
42
|
+
*/
|
|
43
|
+
function basenameNoExt(relativePathPosix) {
|
|
44
|
+
const last = relativePathPosix.lastIndexOf('/')
|
|
45
|
+
const base = last >= 0 ? relativePathPosix.slice(last + 1) : relativePathPosix
|
|
46
|
+
const dot = base.lastIndexOf('.')
|
|
47
|
+
return dot > 0 ? base.slice(0, dot) : base
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Перетворює kebab-case ідентифікатор у camelCase.
|
|
52
|
+
* @param {string} kebab kebab-case рядок (`pg-write-contract`)
|
|
53
|
+
* @returns {string} camelCase (`pgWriteContract`)
|
|
54
|
+
*/
|
|
55
|
+
export function kebabToCamel(kebab) {
|
|
56
|
+
return kebab.replaceAll(/-([a-z0-9])/gu, (_m, c) => c.toUpperCase())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Чи відповідає назва файла канонічному шаблону для каталогу conn.
|
|
61
|
+
* @param {string} relativePathPosix відносний posix-шлях файла
|
|
62
|
+
* @returns {boolean} true, якщо basename + ext збігається зі схемою
|
|
63
|
+
*/
|
|
64
|
+
export function isConnFileNameValid(relativePathPosix) {
|
|
65
|
+
const last = relativePathPosix.lastIndexOf('/')
|
|
66
|
+
const base = last >= 0 ? relativePathPosix.slice(last + 1) : relativePathPosix
|
|
67
|
+
return CONN_FILENAME_RE.test(base)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Збирає всі імена named-експортів у програмі.
|
|
72
|
+
*
|
|
73
|
+
* Покриває: `export const/let/var X`, `export function X`, `export class X`,
|
|
74
|
+
* `export { X }`, `export { X as Y }` (повертає `Y`). `export *` ігнорується
|
|
75
|
+
* (немає конкретного імені для звірки), `export default` обробляється окремо.
|
|
76
|
+
* @param {unknown} program AST root
|
|
77
|
+
* @returns {string[]} список експортованих імен
|
|
78
|
+
*/
|
|
79
|
+
function collectNamedExportNames(program) {
|
|
80
|
+
/** @type {string[]} */
|
|
81
|
+
const out = []
|
|
82
|
+
if (!program || typeof program !== 'object') return out
|
|
83
|
+
const body = /** @type {Record<string, unknown>} */ (program).body
|
|
84
|
+
if (!Array.isArray(body)) return out
|
|
85
|
+
for (const node of body) {
|
|
86
|
+
if (!node || typeof node !== 'object') continue
|
|
87
|
+
const rec = /** @type {Record<string, unknown>} */ (node)
|
|
88
|
+
if (rec.type !== 'ExportNamedDeclaration') continue
|
|
89
|
+
const decl = /** @type {Record<string, unknown> | null} */ (rec.declaration ?? null)
|
|
90
|
+
if (decl) {
|
|
91
|
+
// export const X = ... / export let / export var
|
|
92
|
+
if (decl.type === 'VariableDeclaration' && Array.isArray(decl.declarations)) {
|
|
93
|
+
for (const d of decl.declarations) {
|
|
94
|
+
const id = /** @type {Record<string, unknown> | null} */ (d?.id ?? null)
|
|
95
|
+
if (id && id.type === 'Identifier' && typeof id.name === 'string') out.push(id.name)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// export function X / export class X
|
|
99
|
+
if (
|
|
100
|
+
(decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&
|
|
101
|
+
decl.id &&
|
|
102
|
+
typeof decl.id === 'object' &&
|
|
103
|
+
typeof /** @type {Record<string, unknown>} */ (decl.id).name === 'string'
|
|
104
|
+
) {
|
|
105
|
+
out.push(/** @type {string} */ (/** @type {Record<string, unknown>} */ (decl.id).name))
|
|
106
|
+
}
|
|
107
|
+
} else if (Array.isArray(rec.specifiers)) {
|
|
108
|
+
// export { X } / export { X as Y }
|
|
109
|
+
for (const s of rec.specifiers) {
|
|
110
|
+
const exported = /** @type {Record<string, unknown> | null} */ (s?.exported ?? null)
|
|
111
|
+
if (!exported) continue
|
|
112
|
+
// ESTree: Identifier (name) або Literal (value), залежно від спеки
|
|
113
|
+
if (exported.type === 'Identifier' && typeof exported.name === 'string') out.push(exported.name)
|
|
114
|
+
else if (typeof exported.value === 'string') out.push(exported.value)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Чи є в програмі `export default ...`.
|
|
123
|
+
* @param {unknown} program AST root
|
|
124
|
+
* @returns {boolean} true, якщо знайдено будь-який ExportDefaultDeclaration
|
|
125
|
+
*/
|
|
126
|
+
function hasDefaultExport(program) {
|
|
127
|
+
if (!program || typeof program !== 'object') return false
|
|
128
|
+
const body = /** @type {Record<string, unknown>} */ (program).body
|
|
129
|
+
if (!Array.isArray(body)) return false
|
|
130
|
+
for (const node of body) {
|
|
131
|
+
if (node && typeof node === 'object' && /** @type {Record<string, unknown>} */ (node).type === 'ExportDefaultDeclaration') {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Знаходить порушення правил для одного файла з каталогу conn.
|
|
140
|
+
*
|
|
141
|
+
* Якщо AST не парситься — повертає порожній масив (синтаксис падає в інших перевірках,
|
|
142
|
+
* не дублюємо).
|
|
143
|
+
* @param {string} content вихідний код файла
|
|
144
|
+
* @param {string} relativePathPosix відносний posix-шлях файла (від кореня пакета)
|
|
145
|
+
* @returns {{ kind: 'name' | 'default-export' | 'export-name', expectedName?: string, foundNames?: string[] }[]} список порушень
|
|
146
|
+
*/
|
|
147
|
+
export function findConnFileRuleViolations(content, relativePathPosix) {
|
|
148
|
+
/** @type {{ kind: 'name' | 'default-export' | 'export-name', expectedName?: string, foundNames?: string[] }[]} */
|
|
149
|
+
const out = []
|
|
150
|
+
if (!isConnFileNameValid(relativePathPosix)) {
|
|
151
|
+
out.push({ kind: 'name' })
|
|
152
|
+
// якщо назва нестандартна — далі звірку імені експорту не робимо (camelCase двозначний)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const program = parseProgramOrNull(content, relativePathPosix)
|
|
156
|
+
if (!program) return out
|
|
157
|
+
|
|
158
|
+
if (hasDefaultExport(program)) {
|
|
159
|
+
out.push({ kind: 'default-export' })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (out.some(v => v.kind === 'name')) return out
|
|
163
|
+
|
|
164
|
+
const expected = kebabToCamel(basenameNoExt(relativePathPosix.slice(relativePathPosix.lastIndexOf('/') + 1)))
|
|
165
|
+
const names = collectNamedExportNames(program)
|
|
166
|
+
if (!names.includes(expected)) {
|
|
167
|
+
out.push({ kind: 'export-name', expectedName: expected, foundNames: names })
|
|
168
|
+
}
|
|
169
|
+
return out
|
|
170
|
+
}
|