@nitra/cursor 1.8.144 → 1.8.147

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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Перевіряє правило js-bun-db.mdc.
3
+ *
4
+ * 1) У жодному `package.json` (включно з workspace-пакетами) у `dependencies` не повинно
5
+ * бути `pg` чи `mysql2` — ці бібліотеки треба замінити на Bun native SQL
6
+ * (`import { sql, SQL } from 'bun'`, https://bun.com/docs/runtime/sql).
7
+ *
8
+ * 2) Якщо в коді використовується Bun SQL (імпорт `sql`/`SQL` з `'bun'`), додатково
9
+ * перевіряє небезпечні патерни:
10
+ * - `new SQL(...)` всередині функції (пул має бути singleton на рівні модуля).
11
+ * - `sql.unsafe(\`...${expr}...\`)` (інтерполяція даних у `unsafe` ламає параметризацію).
12
+ * - Динамічні SQL-списки через `.join(',')` у `IN (...)` / `VALUES (...)`
13
+ * (треба `sql([...])`).
14
+ */
15
+ import { existsSync } from 'node:fs'
16
+ import { readFile } from 'node:fs/promises'
17
+ import { join, relative, sep } from 'node:path'
18
+
19
+ import { createCheckReporter } from './utils/check-reporter.mjs'
20
+ import {
21
+ findBunSqlPerRequestConnectionInText,
22
+ findUnsafeBunSqlDynamicSqlListInText,
23
+ findUnsafeBunSqlUnsafeCallInText,
24
+ isBunSqlScanSourceFile,
25
+ textHasBunSqlImport
26
+ } from './utils/bun-sql-scan.mjs'
27
+ import { walkDir } from './utils/walkDir.mjs'
28
+
29
+ /** Імена забороненої залежності у будь-якому `package.json`. */
30
+ const FORBIDDEN_DEPENDENCIES = Object.freeze(['pg', 'mysql2'])
31
+
32
+ /**
33
+ * @param {unknown} v parsed JSON
34
+ * @returns {Record<string, unknown>} object або {}
35
+ */
36
+ function asObject(v) {
37
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return {}
38
+ return /** @type {Record<string, unknown>} */ (v)
39
+ }
40
+
41
+ /**
42
+ * Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
43
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
44
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
45
+ */
46
+ async function findAllPackageJsonPaths(repoRoot) {
47
+ /** @type {string[]} */
48
+ const paths = []
49
+ await walkDir(repoRoot, absPath => {
50
+ if (absPath.endsWith(`${sep}package.json`)) {
51
+ paths.push(absPath)
52
+ }
53
+ })
54
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
55
+ return paths
56
+ }
57
+
58
+ /**
59
+ * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
60
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
61
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
62
+ */
63
+ async function findAllSourcePathsForBunSqlScan(repoRoot) {
64
+ /** @type {string[]} */
65
+ const paths = []
66
+ await walkDir(repoRoot, absPath => {
67
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
68
+ if (isBunSqlScanSourceFile(rel)) {
69
+ paths.push(absPath)
70
+ }
71
+ })
72
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
73
+ return paths
74
+ }
75
+
76
+ /**
77
+ * Перевіряє, чи в кореневому `package.json` присутні заборонені пакети у `dependencies`.
78
+ * @param {string[]} pkgJsonPaths абсолютні шляхи всіх `package.json` у репо
79
+ * @param {string} repoRoot абсолютний шлях до кореня
80
+ * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
81
+ * @returns {Promise<number>} кількість знайдених порушень
82
+ */
83
+ async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
84
+ const { pass, fail } = reporter
85
+ let bad = 0
86
+ for (const absPath of pkgJsonPaths) {
87
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
88
+ let parsed
89
+ try {
90
+ parsed = JSON.parse(await readFile(absPath, 'utf8'))
91
+ } catch {
92
+ fail(`js-bun-db: ${rel} — невалідний JSON`)
93
+ bad++
94
+ continue
95
+ }
96
+ const deps = asObject(parsed.dependencies)
97
+ for (const name of FORBIDDEN_DEPENDENCIES) {
98
+ if (Object.hasOwn(deps, name)) {
99
+ bad++
100
+ fail(
101
+ `js-bun-db: ${rel}: dependencies.${name} — замінити на Bun native SQL ` +
102
+ `(import { sql, SQL } from 'bun', https://bun.com/docs/runtime/sql) (js-bun-db.mdc)`
103
+ )
104
+ }
105
+ }
106
+ }
107
+ if (bad === 0) {
108
+ pass(`js-bun-db: жоден package.json не містить ${FORBIDDEN_DEPENDENCIES.join(' / ')} у dependencies`)
109
+ }
110
+ return bad
111
+ }
112
+
113
+ /**
114
+ * Сканує JS/TS-джерела на небезпечні патерни Bun SQL.
115
+ * @param {string[]} sourcePaths абсолютні шляхи джерел
116
+ * @param {string} repoRoot абсолютний шлях до кореня
117
+ * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
118
+ * @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number }>}
119
+ * `hasBunSqlImport` — чи знайдено хоч один `import { sql|SQL } from 'bun'` у джерелах;
120
+ * решта — кількість порушень кожного типу.
121
+ */
122
+ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
123
+ const { fail } = reporter
124
+ let hasBunSqlImport = false
125
+ let perRequest = 0
126
+ let unsafeCall = 0
127
+ let dynamicList = 0
128
+
129
+ for (const absPath of sourcePaths) {
130
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
131
+ const content = await readFile(absPath, 'utf8')
132
+ if (!hasBunSqlImport && textHasBunSqlImport(content)) {
133
+ hasBunSqlImport = true
134
+ }
135
+
136
+ for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
137
+ perRequest++
138
+ fail(
139
+ `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
140
+ `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
141
+ )
142
+ }
143
+ for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
144
+ unsafeCall++
145
+ fail(
146
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
147
+ `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
148
+ )
149
+ }
150
+ for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
151
+ dynamicList++
152
+ fail(
153
+ `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
154
+ `у IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
155
+ )
156
+ }
157
+ }
158
+
159
+ return { hasBunSqlImport, perRequest, unsafeCall, dynamicList }
160
+ }
161
+
162
+ /**
163
+ * Перевіряє відповідність проєкту правилу js-bun-db.mdc
164
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
165
+ */
166
+ export async function check() {
167
+ const reporter = createCheckReporter()
168
+ const { pass } = reporter
169
+
170
+ const repoRoot = process.cwd()
171
+ const rootPkg = join(repoRoot, 'package.json')
172
+ if (!existsSync(rootPkg)) {
173
+ pass('js-bun-db: package.json у корені відсутній — перевірку пропущено')
174
+ return reporter.getExitCode()
175
+ }
176
+
177
+ const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot)
178
+ if (pkgJsonPaths.length === 0) {
179
+ pass('js-bun-db: package.json не знайдено — перевірку пропущено')
180
+ return reporter.getExitCode()
181
+ }
182
+
183
+ await checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter)
184
+
185
+ const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot)
186
+ if (sourcePaths.length === 0) {
187
+ pass('js-bun-db: немає JS/TS файлів для скану патернів Bun SQL')
188
+ return reporter.getExitCode()
189
+ }
190
+
191
+ const { hasBunSqlImport, perRequest, unsafeCall, dynamicList } = await scanSourcesForBunSqlPatterns(
192
+ sourcePaths,
193
+ repoRoot,
194
+ reporter
195
+ )
196
+
197
+ if (!hasBunSqlImport) {
198
+ pass("js-bun-db: Bun SQL не використовується в коді (немає import { sql|SQL } from 'bun')")
199
+ return reporter.getExitCode()
200
+ }
201
+
202
+ if (perRequest === 0) {
203
+ pass('js-bun-db: немає створення new SQL(...) всередині функцій (singleton на рівні модуля)')
204
+ }
205
+ if (unsafeCall === 0) {
206
+ pass('js-bun-db: немає небезпечних викликів sql.unsafe з інтерполяцією в шаблонному рядку')
207
+ }
208
+ if (dynamicList === 0) {
209
+ pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
210
+ }
211
+
212
+ return reporter.getExitCode()
213
+ }
@@ -2,18 +2,24 @@
2
2
  * Перевіряє лінт JavaScript за правилом js-lint.mdc.
3
3
  *
4
4
  * Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
5
- * `.oxlintrc.json` з `jsPlugins` (`@e18e/eslint-plugin`) і правилом `e18e/prefer-includes: error`,
6
- * `@nitra/eslint-config` у devDependencies мінімум **3.5.0** (транзитивний `@e18e/eslint-plugin` для oxlint), `.jscpd.json`
7
- * (gitignore, exitCode, reporters, minLines), workflow `lint-js.yml` (checkout@v6, setup-bun-deps,
8
- * bunx без --fix), без prettier, `engines.node` >= 24, `"type": "module"` у кореневому
9
- * і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` заборонено.
5
+ * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
6
+ * plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
7
+ * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.6.12** (транзитивний
8
+ * `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
9
+ * `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
10
+ * `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
10
11
  */
11
12
  import { existsSync } from 'node:fs'
12
13
  import { readFile } from 'node:fs/promises'
14
+ import { dirname, join } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
13
16
 
14
17
  import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
15
18
  import { createCheckReporter } from './utils/check-reporter.mjs'
16
19
 
20
+ /** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
21
+ export const OXLINT_CANONICAL_JSON_PATH = join(dirname(fileURLToPath(import.meta.url)), 'utils', 'oxlint-canonical.json')
22
+
17
23
  /** Очікуваний локальний скрипт. */
18
24
  export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
19
25
 
@@ -43,9 +49,9 @@ export function isCanonicalLintJs(script) {
43
49
  }
44
50
 
45
51
  /**
46
- * Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.5.0).
52
+ * Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.6.12).
47
53
  * @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
48
- * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.5.0
54
+ * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.6.12
49
55
  */
50
56
  export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
51
57
  const s = String(versionSpec).trim()
@@ -64,29 +70,97 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
64
70
  }
65
71
 
66
72
  /**
67
- * Перевіряє потрібні поля `.oxlintrc.json` для розширення e18e (js-lint.mdc).
68
- * @param {unknown} cfg корінь JSON-конфігурації oxlint
69
- * @returns {{ ok: boolean, failures: string[] }} `ok` і перелік повідомлень для `fail`
73
+ * Рекурсивне порівняння фрагментів канону oxlint (масиви порядок як у каноні; об’єкти — той самий набір ключів і вкладеність).
74
+ * @param {unknown} actual значення з `.oxlintrc.json`
75
+ * @param {unknown} expected значення з канону
76
+ * @returns {boolean} true, якщо значення збігаються за правилами канону
77
+ */
78
+ function deepEqualOxlintCanonical(actual, expected) {
79
+ if (expected === null || typeof expected !== 'object') {
80
+ return actual === expected
81
+ }
82
+ if (Array.isArray(expected)) {
83
+ return Array.isArray(actual) && JSON.stringify(actual) === JSON.stringify(expected)
84
+ }
85
+ if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
86
+ return false
87
+ }
88
+ const exp = /** @type {Record<string, unknown>} */ (expected)
89
+ const act = /** @type {Record<string, unknown>} */ (actual)
90
+ const expKeys = Object.keys(exp)
91
+ const actKeys = Object.keys(act)
92
+ if (expKeys.length !== actKeys.length) {
93
+ return false
94
+ }
95
+ for (const k of expKeys) {
96
+ if (!(k in act) || !deepEqualOxlintCanonical(act[k], exp[k])) {
97
+ return false
98
+ }
99
+ }
100
+ return true
101
+ }
102
+
103
+ /**
104
+ * Безпечний доступ як до plain-object запису.
105
+ * @param {unknown} v будь-яке значення
106
+ * @returns {Record<string, unknown>} запис або пустий обʼєкт, якщо `v` не plain-object
107
+ */
108
+ function asRecordOrEmpty(v) {
109
+ return v && typeof v === 'object' && !Array.isArray(v) ? /** @type {Record<string, unknown>} */ (v) : {}
110
+ }
111
+
112
+ /**
113
+ * Звіряє блок `rules`: кожне правило з канону має точне збіжне значення в actual.
114
+ * @param {unknown} expected канонічне значення для `rules`
115
+ * @param {unknown} actual поточне значення для `rules`
116
+ * @param {string[]} failures буфер для помилок
117
+ */
118
+ function compareOxlintRules(expected, actual, failures) {
119
+ const er = asRecordOrEmpty(expected)
120
+ const ar = asRecordOrEmpty(actual)
121
+ for (const ruleKey of Object.keys(er)) {
122
+ if (ar[ruleKey] !== er[ruleKey]) {
123
+ failures.push(
124
+ `.oxlintrc.json: rules["${ruleKey}"] очікується ${JSON.stringify(er[ruleKey])}, зараз ${JSON.stringify(ar[ruleKey])}`
125
+ )
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Перевіряє `.oxlintrc.json` проти канону пакета `@nitra/cursor` (усі правила з канону та інші поля з `oxlint-canonical.json`).
132
+ * Додаткові ключі лише в `rules` дозволені; інші поля мають збігатися з каноном.
133
+ * @param {unknown} cfg корінь JSON з `.oxlintrc.json`
134
+ * @param {unknown} canonical розпарений `oxlint-canonical.json`
135
+ * @returns {{ ok: boolean, failures: string[] }} статус і повідомлення для `fail`
70
136
  */
71
- export function verifyOxlintRcE18e(cfg) {
137
+ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
72
138
  const failures = []
73
139
  if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
74
140
  return { ok: false, failures: ['.oxlintrc.json: корінь має бути значенням типу object'] }
75
141
  }
76
- const o = /** @type {Record<string, unknown>} */ (cfg)
77
- const jsPlugins = o.jsPlugins
78
- if (!Array.isArray(jsPlugins) || !jsPlugins.includes('@e18e/eslint-plugin')) {
79
- failures.push('.oxlintrc.json: jsPlugins має містити "@e18e/eslint-plugin"')
142
+ if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical)) {
143
+ return { ok: false, failures: ['внутрішня помилка: канон oxlint має бути object'] }
80
144
  }
81
- const rules = o.rules
82
- if (!rules || typeof rules !== 'object' || Array.isArray(rules)) {
83
- failures.push('.oxlintrc.json: поле rules має бути значенням типу object')
84
- } else {
85
- const r = /** @type {Record<string, unknown>} */ (rules)
86
- if (r['e18e/prefer-includes'] !== 'error') {
87
- failures.push('.oxlintrc.json: у rules має бути "e18e/prefer-includes": "error"')
145
+ const o = /** @type {Record<string, unknown>} */ (cfg)
146
+ const c = /** @type {Record<string, unknown>} */ (canonical)
147
+
148
+ for (const key of Object.keys(c)) {
149
+ const expected = c[key]
150
+ const actual = o[key]
151
+
152
+ if (key === 'rules') {
153
+ compareOxlintRules(expected, actual, failures)
154
+ continue
155
+ }
156
+
157
+ if (!deepEqualOxlintCanonical(actual, expected)) {
158
+ failures.push(
159
+ `.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/scripts/utils/oxlint-canonical.json)`
160
+ )
88
161
  }
89
162
  }
163
+
90
164
  return { ok: failures.length === 0, failures }
91
165
  }
92
166
 
@@ -96,7 +170,7 @@ export function verifyOxlintRcE18e(cfg) {
96
170
  * @param {(msg: string) => void} failFn callback при помилці
97
171
  */
98
172
  async function checkEslintConfig(passFn, failFn) {
99
- let eslintPath = ''
173
+ let eslintPath
100
174
  if (existsSync('eslint.config.js')) {
101
175
  eslintPath = 'eslint.config.js'
102
176
  passFn('eslint.config.js існує')
@@ -157,10 +231,12 @@ function checkPackageJsonLintDeps(pkg, passFn, failFn) {
157
231
  if (nitraEslint) {
158
232
  passFn('@nitra/eslint-config є в devDependencies')
159
233
  if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
160
- passFn('@nitra/eslint-config: мінімум 3.5.0 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)')
234
+ passFn(
235
+ '@nitra/eslint-config: мінімум 3.6.12 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)'
236
+ )
161
237
  } else {
162
238
  failFn(
163
- '@nitra/eslint-config: онови до мінімум "^3.5.0" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
239
+ '@nitra/eslint-config: онови до мінімум "^3.6.12" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
164
240
  )
165
241
  }
166
242
  } else {
@@ -269,9 +345,16 @@ async function checkOxlintRc(passFn, failFn) {
269
345
  return
270
346
  }
271
347
  passFn('.oxlintrc.json існує')
272
- const oxV = verifyOxlintRcE18e(oxCfg)
348
+ let canonical
349
+ try {
350
+ canonical = JSON.parse(await readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8'))
351
+ } catch {
352
+ failFn('внутрішня помилка: не вдалося прочитати канон oxlint з пакета @nitra/cursor')
353
+ return
354
+ }
355
+ const oxV = verifyOxlintRcAgainstCanonical(oxCfg, canonical)
273
356
  if (oxV.ok) {
274
- passFn('.oxlintrc.json: jsPlugins з @e18e/eslint-plugin і e18e/prefer-includes: error')
357
+ passFn('.oxlintrc.json збігається з каноном oxlint (@nitra/cursor)')
275
358
  } else {
276
359
  for (const msg of oxV.failures) {
277
360
  failFn(msg)