@nitra/cursor 1.8.145 → 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.
@@ -69,6 +69,42 @@ function isEsbuildScanFile(relPosix) {
69
69
  )
70
70
  }
71
71
 
72
+ /**
73
+ * Збирає `esbuild`-матчі по рядках одного файлу, поки буфер не досягне ліміту.
74
+ * @param {string} rel relative path
75
+ * @param {string} content вміст файлу
76
+ * @param {{ rel: string; line: number; snippet: string }[]} matches буфер для збору матчів
77
+ * @param {number} maxMatches максимум елементів у буфері
78
+ */
79
+ function appendEsbuildLineMatches(rel, content, matches, maxMatches) {
80
+ const lines = content.split('\n')
81
+ for (const [i, line] of lines.entries()) {
82
+ if (matches.length >= maxMatches) return
83
+ if (ESBUILD_RE.test(line)) {
84
+ matches.push({ rel, line: i + 1, snippet: line.trim() })
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Перебирає вибрані файли пакета і збирає до `maxMatches` згадок `esbuild`.
91
+ * @param {string} absPackageRoot абсолютний шлях до кореня пакета
92
+ * @param {{ rel: string }[]} files перелік відносних шляхів
93
+ * @param {number} maxMatches максимум знайдених матчів
94
+ * @returns {Promise<{ rel: string; line: number; snippet: string }[]>} зібрані матчі
95
+ */
96
+ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
97
+ /** @type {{ rel: string; line: number; snippet: string }[]} */
98
+ const matches = []
99
+ for (const { rel } of files) {
100
+ if (matches.length >= maxMatches) break
101
+ const content = await readFile(join(absPackageRoot, rel), 'utf8')
102
+ if (!ESBUILD_RE.test(content)) continue
103
+ appendEsbuildLineMatches(rel, content, matches, maxMatches)
104
+ }
105
+ return matches
106
+ }
107
+
72
108
  /**
73
109
  * Сканує дерево пакета на згадки `esbuild` і підказує заміну на `rolldown`.
74
110
  * @param {string} rootDir відносний шлях до пакета
@@ -78,31 +114,16 @@ function isEsbuildScanFile(relPosix) {
78
114
  * @param {(msg: string) => void} fail callback при помилці
79
115
  */
80
116
  async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fail) {
81
- /** @type {{ rel: string; line: number; snippet: string }[]} */
82
- const hits = []
83
-
117
+ /** @type {{ rel: string }[]} */
118
+ const candidates = []
84
119
  await walkDir(absPackageRoot, absPath => {
85
120
  const rel = relative(absPackageRoot, absPath).split('\\').join('/')
86
121
  if (!isEsbuildScanFile(rel)) return
87
- hits.push({ rel, line: 0, snippet: '' })
122
+ candidates.push({ rel })
88
123
  })
89
124
 
90
- // ми використали hits як буфер шляхів; зараз перетворимо на реальні співпадіння
91
- /** @type {{ rel: string; line: number; snippet: string }[]} */
92
- const matches = []
93
- for (const { rel } of hits) {
94
- const content = await readFile(join(absPackageRoot, rel), 'utf8')
95
- if (!ESBUILD_RE.test(content)) continue
96
-
97
- const lines = content.split('\n')
98
- for (let i = 0; i < lines.length; i++) {
99
- if (ESBUILD_RE.test(lines[i])) {
100
- matches.push({ rel, line: i + 1, snippet: lines[i].trim() })
101
- if (matches.length >= 30) break
102
- }
103
- }
104
- if (matches.length >= 30) break
105
- }
125
+ const maxMatches = 30
126
+ const matches = await collectEsbuildMatchesInFiles(absPackageRoot, candidates, maxMatches)
106
127
 
107
128
  if (matches.length === 0) {
108
129
  passFn(`${prefix}немає згадок 'esbuild' у джерелах пакета (очікується rolldown)`)
@@ -112,8 +133,8 @@ async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fai
112
133
  for (const m of matches) {
113
134
  fail(`${prefix}${m.rel}:${m.line} — знайдено 'esbuild'. Замінити на 'rolldown'. Фрагмент: ${m.snippet}`)
114
135
  }
115
- if (matches.length >= 30) {
116
- fail(`${prefix}показано перші 30 збігів 'esbuild' (замінити на 'rolldown')`)
136
+ if (matches.length >= maxMatches) {
137
+ fail(`${prefix}показано перші ${maxMatches} збігів 'esbuild' (замінити на 'rolldown')`)
117
138
  }
118
139
  }
119
140
 
@@ -308,44 +329,60 @@ async function checkVuePackage(rootDir, fail, passFn) {
308
329
  }
309
330
 
310
331
  /**
311
- * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
312
- * @returns {Promise<number>} 0 все OK, 1 — є проблеми
332
+ * Збирає корені пакетів, у яких у `dependencies` є `vue`.
333
+ * @param {string[]} roots усі корені пакетів monorepo
334
+ * @returns {Promise<string[]>} перелік пакетів з vue у dependencies
313
335
  */
314
- export async function check() {
315
- const reporter = createCheckReporter()
316
- const { pass, fail } = reporter
317
-
318
- const roots = await getMonorepoPackageRootDirs()
336
+ async function collectVueRoots(roots) {
319
337
  /** @type {string[]} */
320
338
  const vueRoots = []
321
339
  for (const r of roots) {
322
340
  const p = join(r, 'package.json')
323
- if (existsSync(p)) {
324
- const pkg = JSON.parse(await readFile(p, 'utf8'))
325
- if (pkg.dependencies?.vue) vueRoots.push(r)
326
- }
341
+ if (!existsSync(p)) continue
342
+ const pkg = JSON.parse(await readFile(p, 'utf8'))
343
+ if (pkg.dependencies?.vue) vueRoots.push(r)
327
344
  }
345
+ return vueRoots
346
+ }
328
347
 
329
- if (vueRoots.length > 0) {
330
- if (existsSync('.vscode/extensions.json')) {
331
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
332
- if (ext.recommendations?.includes('Vue.volar')) {
333
- pass('extensions.json містить Vue.volar')
334
- } else {
335
- fail('extensions.json не містить Vue.volar — додай до recommendations')
336
- }
337
- } else {
338
- fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
339
- }
348
+ /**
349
+ * Перевіряє наявність рекомендації `Vue.volar` у `.vscode/extensions.json`.
350
+ * @param {(msg: string) => void} pass pass callback
351
+ * @param {(msg: string) => void} fail fail callback
352
+ * @returns {Promise<void>}
353
+ */
354
+ async function checkVueVolarRecommendation(pass, fail) {
355
+ if (!existsSync('.vscode/extensions.json')) {
356
+ fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
357
+ return
358
+ }
359
+ const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
360
+ if (ext.recommendations?.includes('Vue.volar')) {
361
+ pass('extensions.json містить Vue.volar')
340
362
  } else {
341
- pass('Vue.volar: пропущено repo немає пакетів з vue у dependencies)')
363
+ fail('extensions.json не містить Vue.volar додай до recommendations')
342
364
  }
365
+ }
366
+
367
+ /**
368
+ * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
369
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
370
+ */
371
+ export async function check() {
372
+ const reporter = createCheckReporter()
373
+ const { pass, fail } = reporter
374
+
375
+ const roots = await getMonorepoPackageRootDirs()
376
+ const vueRoots = await collectVueRoots(roots)
343
377
 
344
378
  if (vueRoots.length === 0) {
379
+ pass('Vue.volar: пропущено (у repo немає пакетів з vue у dependencies)')
345
380
  pass('vue не знайдено в dependencies жодного пакета (перевірка vue пропущена)')
346
381
  return reporter.getExitCode()
347
382
  }
348
383
 
384
+ await checkVueVolarRecommendation(pass, fail)
385
+
349
386
  for (const r of vueRoots) {
350
387
  await checkVuePackage(r, fail, pass)
351
388
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Визначення, чи виконується поточний ESM-модуль як точка входу CLI, а не як import у тестах чи інших модулях.
3
3
  *
4
- * У Bun використовується `import.meta.main`; у Node — порівняння `import.meta.url` з `process.argv[1]`
5
- * після `resolve`, щоб `bun path/to/script.mjs` і `node path/to/script.mjs` коректно вважалися прямим запуском.
4
+ * Порівняння `import.meta.url` з `process.argv[1]` після `resolve`, щоб `bun path/to/script.mjs`
5
+ * і `node path/to/script.mjs` коректно вважалися прямим запуском.
6
6
  */
7
7
  import { resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
@@ -12,9 +12,6 @@ import { fileURLToPath } from 'node:url'
12
12
  * @returns {boolean} `true`, якщо файл запущено напряму; інакше `false`.
13
13
  */
14
14
  export function isRunAsCli() {
15
- if (import.meta.main === true) {
16
- return true
17
- }
18
15
  try {
19
16
  const entry = process.argv[1]
20
17
  if (!entry) {
@@ -108,7 +108,7 @@ async function runBunInstall(projectRoot) {
108
108
  } catch (error) {
109
109
  const exitCode = typeof error?.code === 'number' ? error.code : null
110
110
  if (exitCode !== null && exitCode !== 0) {
111
- throw new Error(`bun i завершився з кодом ${exitCode}`)
111
+ throw new Error(`bun i завершився з кодом ${exitCode}`, { cause: error })
112
112
  }
113
113
  throw error
114
114
  }
@@ -278,7 +278,7 @@ export function findUnsafeBunSqlDynamicSqlListInText(content, virtualPath = 'sca
278
278
  * Скан по сирому тексту — без AST, щоб бути дешевим: викликається на кожному
279
279
  * JS/TS-файлі при зборі ознак для авто-детекту правил.
280
280
  * @param {string} content вміст файлу
281
- * @returns {boolean}
281
+ * @returns {boolean} true, якщо є імпорт sql або SQL з модуля bun
282
282
  */
283
283
  export function textHasBunSqlImport(content) {
284
284
  return BUN_SQL_IMPORT_RE.test(content)
@@ -307,7 +307,6 @@ export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
307
307
  *
308
308
  * Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
309
309
  * потрапляє “готовий шматок SQL”, а не параметризовані значення.
310
- *
311
310
  * @param {string} content вихідний код
312
311
  * @param {string} [virtualPath] шлях для вибору `lang`
313
312
  * @returns {{ line: number, snippet: string }[]} список порушень
@@ -371,6 +370,24 @@ function isLiteralNumericArrayExpression(node) {
371
370
  })
372
371
  }
373
372
 
373
+ /**
374
+ * Чи це безпосередній виклик числового парсера (parseInt/parseFloat/Number/BigInt)
375
+ * або обʼєктний доступ до них (наприклад `Number.parseInt(...)`).
376
+ * @param {Record<string, unknown>} node AST CallExpression
377
+ * @returns {boolean} true, якщо callee — числовий парсер
378
+ */
379
+ function isNumericParseCallExpression(node) {
380
+ if (node.type !== 'CallExpression') return false
381
+ const callee = node.callee
382
+ if (!callee) return false
383
+ if (callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) return true
384
+ if (callee.type === 'MemberExpression' && !callee.computed) {
385
+ const prop = callee.property
386
+ return !!prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)
387
+ }
388
+ return false
389
+ }
390
+
374
391
  /**
375
392
  * Чи містить піддерево виклик числового парсера (parseInt/parseFloat/Number/BigInt)
376
393
  * або унарний `+` (приведення до Number). Це сигнал, що значення гарантовано числове
@@ -380,20 +397,9 @@ function isLiteralNumericArrayExpression(node) {
380
397
  */
381
398
  function subtreeHasNumericParseCall(node) {
382
399
  if (!node || typeof node !== 'object') return false
383
- if (Array.isArray(node)) return node.some(subtreeHasNumericParseCall)
400
+ if (Array.isArray(node)) return node.some(item => subtreeHasNumericParseCall(item))
384
401
 
385
- if (node.type === 'CallExpression') {
386
- const callee = node.callee
387
- if (callee && callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) {
388
- return true
389
- }
390
- if (callee && callee.type === 'MemberExpression' && !callee.computed) {
391
- const prop = callee.property
392
- if (prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)) {
393
- return true
394
- }
395
- }
396
- }
402
+ if (isNumericParseCallExpression(node)) return true
397
403
  if (node.type === 'UnaryExpression' && node.operator === '+') return true
398
404
 
399
405
  for (const key of Object.keys(node)) {
@@ -426,7 +432,6 @@ function collectVariableDeclarators(programNode) {
426
432
  *
427
433
  * Якщо для Identifier немає видимого init (наприклад параметр функції чи import),
428
434
  * вираз вважається не парсованим — потрібен явний парсер на місці підстановки.
429
- *
430
435
  * @param {unknown} expr вираз з template.expressions
431
436
  * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и файлу
432
437
  * @param {Set<string>} [seen] іменa Identifier-ів, що вже трасуються (анти-цикл)
@@ -461,19 +466,52 @@ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
461
466
  }
462
467
 
463
468
  /**
464
- * Знаходить підстановки `IN (${expr})` у TemplateLiteral, де `expr` не пройшов числовий парсер.
465
- *
466
- * Навіть у безпечному `pool.request().query\`...\`` краще явно парсити значення (parseInt/
467
- * Number/BigInt/parseFloat) та фільтрувати NaN — це гарантує, що жодний елемент не може
468
- * містити SQL-метасимволи, навіть якщо колись query-функція або обгортка зміняться. У
469
- * небезпечних контекстах (наприклад `pool.query(String.raw\`...\`)`) це єдиний бар'єр від SQL
470
- * injection.
471
- *
472
- * Випадки `${arr.join(',')}` свідомо ігноруються — їх ловить
473
- * {@link findUnsafeMssqlDynamicSqlListInText}.
469
+ * Сирий текст quasi-елемента TemplateLiteral на позиції перед expressions[i].
470
+ * @param {unknown} q quasi-елемент TemplateLiteral
471
+ * @returns {string} `q.value.raw` або порожній рядок, якщо структура не підходить
472
+ */
473
+ function quasiRawText(q) {
474
+ return q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
475
+ ? q.value.raw
476
+ : ''
477
+ }
478
+
479
+ /**
480
+ * Збирає порушення для одного TemplateLiteral вузла: знаходить expressions, що
481
+ * стоять одразу після `IN (` без числового парсера значень.
482
+ * @param {Record<string, unknown>} node TemplateLiteral
483
+ * @param {string} content вихідний код
484
+ * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и для трасування
485
+ * @param {{ line: number, snippet: string }[]} out буфер результатів
486
+ */
487
+ function collectInListUnparsedFromTemplate(node, content, declarators, out) {
488
+ if (node.type !== 'TemplateLiteral') return
489
+ const quasis = node.quasis
490
+ const expressions = node.expressions
491
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
492
+
493
+ for (const [i, expr] of expressions.entries()) {
494
+ if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
495
+ if (!expr || typeof expr !== 'object') continue
496
+ if (isJoinCall(expr)) continue
497
+ if (isInListExpressionParsed(expr, declarators)) continue
498
+
499
+ const startOffset = typeof expr.start === 'number' ? expr.start : node.start
500
+ out.push({
501
+ line: offsetToLine(content, startOffset),
502
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
503
+ })
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Знаходить підстановки IN (вираз) у TemplateLiteral, де вираз не пройшов числовий парсер.
474
509
  *
510
+ * Навіть у безпечному tagged template pool.request().query краще явно парсити значення (parseInt,
511
+ * Number, BigInt, parseFloat) та фільтрувати NaN. Див. також findUnsafeMssqlDynamicSqlListInText для
512
+ * випадків arr.join у списках.
475
513
  * @param {string} content вихідний код
476
- * @param {string} [virtualPath] шлях для вибору `lang`
514
+ * @param {string} [virtualPath] шлях для вибору мови парсера (lang)
477
515
  * @returns {{ line: number, snippet: string }[]} список порушень
478
516
  */
479
517
  export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan.ts') {
@@ -490,32 +528,7 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
490
528
 
491
529
  /** @type {{ line: number, snippet: string }[]} */
492
530
  const out = []
493
- walkAstWithAncestors(result.program, [], node => {
494
- if (node.type !== 'TemplateLiteral') return
495
- const quasis = node.quasis
496
- const expressions = node.expressions
497
- if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
498
-
499
- for (let i = 0; i < expressions.length; i++) {
500
- const q = quasis[i]
501
- const rawText =
502
- q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
503
- ? q.value.raw
504
- : ''
505
- if (!IN_PLACEHOLDER_END_RE.test(rawText)) continue
506
-
507
- const expr = expressions[i]
508
- if (!expr || typeof expr !== 'object') continue
509
- if (isJoinCall(expr)) continue
510
- if (isInListExpressionParsed(expr, declarators)) continue
511
-
512
- const startOffset = typeof expr.start === 'number' ? expr.start : node.start
513
- out.push({
514
- line: offsetToLine(content, startOffset),
515
- snippet: normalizeSnippet(content.slice(node.start, node.end))
516
- })
517
- }
518
- })
531
+ walkAstWithAncestors(result.program, [], node => collectInListUnparsedFromTemplate(node, content, declarators, out))
519
532
 
520
533
  return out
521
534
  }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["unicorn", "oxc", "import", "jsdoc", "promise", "node"],
4
+ "jsPlugins": ["@e18e/eslint-plugin"],
5
+ "categories": {},
6
+ "rules": {},
7
+ "settings": {
8
+ "next": {
9
+ "rootDir": []
10
+ },
11
+ "jsdoc": {
12
+ "ignorePrivate": false,
13
+ "ignoreInternal": false,
14
+ "ignoreReplacesDocs": true,
15
+ "overrideReplacesDocs": true,
16
+ "augmentsExtendsReplacesDocs": false,
17
+ "implementsReplacesDocs": false,
18
+ "exemptDestructuredRootsFromChecks": false,
19
+ "tagNamePreference": {}
20
+ }
21
+ },
22
+ "env": {
23
+ "builtin": true
24
+ },
25
+ "globals": {},
26
+ "ignorePatterns": []
27
+ }