@nitra/cursor 1.8.185 → 1.8.188

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 CHANGED
@@ -4,6 +4,34 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.188] - 2026-05-07
8
+
9
+ ### Changed
10
+
11
+ - `vue` (mdc v1.6 → v1.7): для Volar/асетів канонічно лише **`jsconfig.json`** у корені пакета — прибрано альтернативу з `tsconfig.json`. `check-vue.mjs`: перевіряється лише наявність `jsconfig.json`.
12
+
13
+ ## [1.8.187] - 2026-05-07
14
+
15
+ ### Added
16
+
17
+ - `check-vue.mjs`: перевірка `src/vite-env.d.ts` з `/// <reference types="vite/client" />` та наявності `jsconfig.json` або `tsconfig.json` у корені кожного Vue-пакета (типи для імпортів асетів у `.vue`).
18
+
19
+ ### Changed
20
+
21
+ - `vue` (mdc v1.5 → v1.6): секція **«Vite client types (Volar, імпорти асетів)»** — обов’язкові `vite-env.d.ts`, jsconfig/tsconfig; застереження щодо вузького `compilerOptions.types`. Оновлено блок **«Перевірка»**.
22
+
23
+ ## [1.8.186] - 2026-05-07
24
+
25
+ ### Added
26
+
27
+ - `check-js-run.mjs` + `scripts/utils/promise-settimeout-scan.mjs`: програмна перевірка нової секції js-run «Паузи через setTimeout». AST-сканер на `oxc-parser` ловить `new Promise(resolve => setTimeout(resolve, ms))` (з `await` чи без, arrow та function expression, concise та block body, тривіально загорнутий callback `() => resolve()`). Паттерни з передачею значення (`r => setTimeout(() => r(value), ms)`), іншим callback-ом замість resolve, або з додатковими стейтментами в блоці — поза правилом (це не «чиста» пауза).
28
+ - `tests/promise-settimeout-scan.test.mjs`: 13 модульних тестів (await/без, block-body, function expression, обгорнутий callback, false-positive guards, multiline номер рядка, кілька входжень, фільтр розширень).
29
+ - `tests/check-js-run-fixture.test.mjs`: 2 інтеграційні кейси на `check()` — fail при `await new Promise(r => setTimeout(r, ms))` у workspace-пакеті, pass при `await setTimeout(ms)` з `node:timers/promises`.
30
+
31
+ ### Changed
32
+
33
+ - `js-run` (mdc v1.3 → v1.4): додано секцію **«Паузи через setTimeout»** — заборонено `await new Promise(resolve => setTimeout(resolve, ms))`, замість цього треба `await setTimeout(ms)` з `node:timers/promises`. Зауваження про затінення глобального `setTimeout` у тому ж файлі (за потреби callback-варіант імпортувати під іншим іменем, наприклад `setTimeoutCb` з `node:timers`).
34
+
7
35
  ## [1.8.185] - 2026-05-06
8
36
 
9
37
  ### Changed
package/mdc/js-run.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
3
3
  alwaysApply: true
4
- version: '1.3'
4
+ version: '1.4'
5
5
  ---
6
6
 
7
7
  ## Область застосування
@@ -136,6 +136,18 @@ console.log(env.OPTIONAL_ENV_VAR)
136
136
  `// @nitra/cursor ignore-next-line checkEnv` безпосередньо перед використанням
137
137
  (escape-hatch для legacy-коду, не для нових файлів).
138
138
 
139
+ ## Паузи через setTimeout
140
+
141
+ Заборонено робити паузи через `await new Promise(resolve => setTimeout(resolve, ms))` — таку обгортку треба замінити на promise-варіант `setTimeout` з `node:timers/promises`:
142
+
143
+ ```javascript title="Замість new Promise + setTimeout"
144
+ import { setTimeout } from 'node:timers/promises'
145
+
146
+ await setTimeout(500)
147
+ ```
148
+
149
+ Імпорт `setTimeout` з `node:timers/promises` затіняє глобальний таймер у файлі — якщо в тому ж файлі потрібен callback-варіант, імпортуй його під іншим іменем (наприклад, `import { setTimeout as setTimeoutCb } from 'node:timers'`).
150
+
139
151
  ## depcheck у GitHub Actions з path-фільтром
140
152
 
141
153
  Якщо в `.github/workflows/*.yml` є тригер з `paths:`, який обмежує запуск workflow змінами в каталозі конкретного backend-пакета, в job цього workflow має бути крок `npx depcheck` з `working-directory`, який вказує на той самий каталог пакета. Це гарантує, що декларація залежностей у `package.json` пакета відповідає реальним імпортам — інакше можна випадково зламати білд після видалення «зайвої» залежності, яка насправді використовується через побічний імпорт.
package/mdc/vue.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Vue
3
3
  alwaysApply: true
4
- version: '1.5'
4
+ version: '1.7'
5
5
  ---
6
6
 
7
7
  # Vue 3 Composition API — правила для .cursorrules
@@ -208,6 +208,45 @@ export default defineConfig({
208
208
  })
209
209
  ```
210
210
 
211
+ ## Vite client types (Volar, імпорти асетів)
212
+
213
+ Без типів **Vite** редактор (Volar / TypeScript) не знає, що імпорт статичного файлу (`import url from './hero.avif'`, `*.png`, `*.svg` тощо) відповідає модулю з `string` URL. Тоді у `.vue` з’являється помилка на кшталт **Cannot find module '…' or its corresponding type declarations**.
214
+
215
+ У **кожному** workspace-пакеті з **Vue + Vite** обов’язково:
216
+
217
+ 1. **`src/vite-env.d.ts`** — рівно з посиланням на клієнтські типи Vite (одного рядка достатньо):
218
+
219
+ ```ts title="src/vite-env.d.ts"
220
+ /// <reference types="vite/client" />
221
+ ```
222
+
223
+ Так підтягуються декларації з `vite/client.d.ts` (`declare module '*.avif'`, `*.png`, …).
224
+
225
+ 2. **Корінь пакета:** **`jsconfig.json`** із **`include`**, що охоплює `src` (наприклад `"include": ["src/**/*"]`), щоб мова служби бачила `vite-env.d.ts` і SFC.
226
+
227
+ Мінімальний приклад для JS-пакета:
228
+
229
+ ```json title="jsconfig.json"
230
+ {
231
+ "compilerOptions": {
232
+ "target": "ESNext",
233
+ "module": "ESNext",
234
+ "moduleResolution": "bundler",
235
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
236
+ "jsx": "preserve",
237
+ "strict": true,
238
+ "noEmit": true,
239
+ "skipLibCheck": true,
240
+ "resolveJsonModule": true,
241
+ "isolatedModules": true,
242
+ "allowJs": true
243
+ },
244
+ "include": ["src/**/*"]
245
+ }
246
+ ```
247
+
248
+ **Не** звужуй без потреби **`compilerOptions.types`** до `["vite/client"]`: це може відрізати інші пакети з `@types` і зламати інші підказки. Достатньо `/// <reference types="vite/client" />` у `vite-env.d.ts` і коректного `include`.
249
+
211
250
  ## Тести
212
251
 
213
252
  Проекту повинен бути покритий тестами E2E за допомогою Playwright.
@@ -287,4 +326,4 @@ import path from 'node:path'
287
326
 
288
327
  ## Перевірка
289
328
 
290
- `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, а також обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
329
+ `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.185",
3
+ "version": "1.8.188",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,7 +21,10 @@
21
21
  * - «depcheck у GitHub Actions з path-фільтром»: для кожного workflow з `paths:`,
22
22
  * обмеженим каталогом цього пакета (`<rootDir>/...`), має бути крок
23
23
  * `npx depcheck --ignores="graphql,bun"` (плюс інші, за потреби) з
24
- * `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`).
24
+ * `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`);
25
+ * - «Паузи через setTimeout»: `new Promise(resolve => setTimeout(resolve, ms))` (з/без `await`)
26
+ * треба замінити на `await setTimeout(ms)` з `node:timers/promises`
27
+ * (див. `utils/promise-settimeout-scan.mjs`).
25
28
  */
26
29
  import { existsSync } from 'node:fs'
27
30
  import { readFile } from 'node:fs/promises'
@@ -42,6 +45,10 @@ import {
42
45
  resolveConnDirFromPackageJson
43
46
  } from './utils/conn-imports-scan.mjs'
44
47
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
48
+ import {
49
+ findPromiseSetTimeoutInText,
50
+ isPromiseSetTimeoutScanSourceFile
51
+ } from './utils/promise-settimeout-scan.mjs'
45
52
  import { walkDir } from './utils/walkDir.mjs'
46
53
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
47
54
 
@@ -162,6 +169,30 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
162
169
  return violations
163
170
  }
164
171
 
172
+ /**
173
+ * Сканує джерела пакета на паттерн `new Promise(resolve => setTimeout(resolve, ms))`.
174
+ * @param {string} absPackageRoot абсолютний корінь пакета
175
+ * @param {string[]} sourcePaths абсолютні шляхи до файлів
176
+ * @param {string} label префікс повідомлення `[<pkg>] `
177
+ * @param {(msg: string) => void} fail callback при помилці
178
+ * @returns {Promise<number>} кількість порушень
179
+ */
180
+ async function checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, fail) {
181
+ let violations = 0
182
+ for (const absPath of sourcePaths) {
183
+ const rel = relPosix(absPackageRoot, absPath)
184
+ if (!isPromiseSetTimeoutScanSourceFile(rel)) continue
185
+ const content = await readFile(absPath, 'utf8')
186
+ for (const v of findPromiseSetTimeoutInText(content, rel)) {
187
+ violations++
188
+ fail(
189
+ `${label}${rel}:${v.line} — заміни 'new Promise(r => setTimeout(r, ms))' на 'await setTimeout(ms)' з 'node:timers/promises': ${v.snippet}`
190
+ )
191
+ }
192
+ }
193
+ return violations
194
+ }
195
+
165
196
  /**
166
197
  * Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
167
198
  * @param {string} rootDir відносний шлях workspace (не `'.'`)
@@ -205,6 +236,11 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
205
236
  )
206
237
  }
207
238
 
239
+ const pauseViolations = await checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, fail)
240
+ if (pauseViolations === 0) {
241
+ passFn(`${label}немає 'new Promise(r => setTimeout(r, ms))' — паузи через 'node:timers/promises'`)
242
+ }
243
+
208
244
  await checkOtelConfigmap(rootDir, label, fail, passFn)
209
245
 
210
246
  checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
@@ -4,6 +4,9 @@
4
4
  * Версії Vite та плагінів, vue-macros, auto-import, layouts, вміст `vite.config`;
5
5
  * у репозиторії — рекомендацію розширення Vue.volar.
6
6
  *
7
+ * У кожному Vue+Vite-пакеті очікується `src/vite-env.d.ts` з `/// <reference types="vite/client" />`
8
+ * та `jsconfig.json` у корені пакета (типи для імпортів асетів у `.vue`).
9
+ *
7
10
  * У `vite.config.*` заборонено використовувати `process.env.npm_lifecycle_event` (Bun не підставляє його як npm),
8
11
  * натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
9
12
  *
@@ -32,6 +35,9 @@ import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
32
35
  const MAJOR_VERSION_RE = /(\d+)/
33
36
  const ESBUILD_RE = /\besbuild\b/
34
37
 
38
+ /** Регулярний вираз для triple-slash `reference types="vite/client"` у `src/vite-env.d.ts`. */
39
+ const VITE_CLIENT_REFERENCE_RE = /\/\/\/\s*<reference\s+types\s*=\s*["']vite\/client["']\s*\/>/
40
+
35
41
  /**
36
42
  * Визначає, чи можна сканувати файл як текст на згадки `esbuild`.
37
43
  * @param {string} relPosix відносний шлях у posix-форматі
@@ -202,6 +208,43 @@ function checkRequiredDep(deps, name, prefix, passFn, fail, hint = `${name} ві
202
208
  * @param {(msg: string) => void} passFn callback при успішній перевірці
203
209
  * @param {(msg: string) => void} fail callback при помилці
204
210
  */
211
+ /**
212
+ * Перевіряє `src/vite-env.d.ts` і наявність `jsconfig.json` для підтягування типів асетів Vite у IDE.
213
+ * @param {string} rootDir відносний шлях до кореня пакета
214
+ * @param {string} prefix префікс повідомлень
215
+ * @param {(msg: string) => void} passFn успіх
216
+ * @param {(msg: string) => void} fail помилка
217
+ * @returns {Promise<void>}
218
+ */
219
+ async function checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail) {
220
+ const envRel = join(rootDir, 'src/vite-env.d.ts')
221
+ if (!existsSync(envRel)) {
222
+ fail(
223
+ `${prefix}немає src/vite-env.d.ts — додай файл з рядком /// <reference types="vite/client" /> ` +
224
+ `(інакше TS/Volar не бачать типів для імпортів асетів: png, avif, css як URL).`
225
+ )
226
+ return
227
+ }
228
+ const envContent = await readFile(envRel, 'utf8')
229
+ if (!VITE_CLIENT_REFERENCE_RE.test(envContent)) {
230
+ fail(
231
+ `${prefix}src/vite-env.d.ts має містити /// <reference types="vite/client" /> ` +
232
+ `(без цього імпорти статичних файлів у .vue дають «Cannot find module … type declarations»).`
233
+ )
234
+ return
235
+ }
236
+ passFn(`${prefix}src/vite-env.d.ts посилається на vite/client`)
237
+
238
+ if (!existsSync(join(rootDir, 'jsconfig.json'))) {
239
+ fail(
240
+ `${prefix}немає jsconfig.json у корені пакета — додай файл з "include": ["src/**/*"] тощо, ` +
241
+ `щоб IDE підхопила vite-env.d.ts і .vue.`
242
+ )
243
+ return
244
+ }
245
+ passFn(`${prefix}jsconfig.json присутній`)
246
+ }
247
+
205
248
  function checkViteVersion(devDeps, prefix, passFn, fail) {
206
249
  const v = devDeps.vite
207
250
  if (!v) {
@@ -442,6 +485,8 @@ async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
442
485
  'vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next'
443
486
  )
444
487
 
488
+ await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail)
489
+
445
490
  const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
446
491
  await checkVueImportViolations(
447
492
  rootDir,
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Знаходить паттерн `new Promise(resolve => setTimeout(resolve, ms))` (з `await` чи без)
3
+ * у джерелах — таку обгортку треба замінити на `setTimeout` з `node:timers/promises`
4
+ * згідно з js-run.mdc, секція «Паузи через setTimeout».
5
+ *
6
+ * Семантика — структурна (без regex по тілу): `NewExpression` з ідентифікатор-callee `Promise`
7
+ * і єдиним аргументом-функцією, тіло якої — виклик `setTimeout(<resolve>, ms)`. Перший
8
+ * аргумент `setTimeout` має передавати `resolve` напряму або тривіально загорнутим у
9
+ * безпараметричну функцію `() => resolve()` / `function () { resolve() }` без жодних
10
+ * аргументів — інакше це не «чиста пауза», і паттерн не вмикається.
11
+ *
12
+ * Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
13
+ * порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
14
+ */
15
+ import { normalizeSnippet, offsetToLine, parseProgramOrNull } from './ast-scan-utils.mjs'
16
+
17
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
18
+
19
+ /**
20
+ * Чи аргумент, який передають у `setTimeout`, — це «голий» виклик `resolve`
21
+ * (тобто сам ідентифікатор або `() => resolve()` без аргументів).
22
+ * @param {Record<string, unknown> | null | undefined} arg AST-вузол першого аргументу `setTimeout`
23
+ * @param {string} paramName ім'я параметра-resolve у тіла-функції Promise
24
+ * @returns {boolean} `true`, якщо це чиста передача resolve без значення
25
+ */
26
+ function isBareResolveCallback(arg, paramName) {
27
+ if (!arg || typeof arg !== 'object') return false
28
+ if (arg.type === 'Identifier' && arg.name === paramName) return true
29
+ if (arg.type !== 'ArrowFunctionExpression' && arg.type !== 'FunctionExpression') return false
30
+ if ((arg.params?.length ?? 0) !== 0) return false
31
+ const callExpr = extractSingleCallExpression(arg.body)
32
+ if (!callExpr) return false
33
+ if (callExpr.callee?.type !== 'Identifier' || callExpr.callee.name !== paramName) return false
34
+ return !Array.isArray(callExpr.arguments) || callExpr.arguments.length === 0
35
+ }
36
+
37
+ /**
38
+ * Якщо тіло функції — рівно один `CallExpression` (концизне `() => foo()` або
39
+ * `{ foo() }` без інших стейтментів), повертає його. Інакше — `null`.
40
+ * @param {unknown} body тіло функції з AST
41
+ * @returns {Record<string, unknown> | null} AST-вузол `CallExpression` або `null`
42
+ */
43
+ function extractSingleCallExpression(body) {
44
+ if (!body || typeof body !== 'object') return null
45
+ if (body.type === 'CallExpression') return body
46
+ if (body.type !== 'BlockStatement') return null
47
+ if (!Array.isArray(body.body) || body.body.length !== 1) return null
48
+ const stmt = body.body[0]
49
+ if (!stmt || stmt.type !== 'ExpressionStatement') return null
50
+ const expr = stmt.expression
51
+ return expr?.type === 'CallExpression' ? expr : null
52
+ }
53
+
54
+ /**
55
+ * Чи це `NewExpression` виду `new Promise(<resolve> => setTimeout(<resolve>, ms))`.
56
+ * Параметр-resolve має бути простим Identifier; setTimeout — глобальним викликом
57
+ * за іменем (з будь-якого джерела — node:timers, global, тощо: значення для нас має
58
+ * лише структурний паттерн).
59
+ * @param {Record<string, unknown> | null | undefined} node AST-вузол
60
+ * @returns {boolean} `true`, якщо це проблемний паттерн «обгортки таймера у Promise»
61
+ */
62
+ function isPromiseSetTimeoutDelay(node) {
63
+ if (!node || node.type !== 'NewExpression') return false
64
+ if (node.callee?.type !== 'Identifier' || node.callee.name !== 'Promise') return false
65
+ if (!Array.isArray(node.arguments) || node.arguments.length !== 1) return false
66
+ const fn = node.arguments[0]
67
+ if (!fn || (fn.type !== 'ArrowFunctionExpression' && fn.type !== 'FunctionExpression')) return false
68
+ if (!Array.isArray(fn.params) || fn.params.length === 0) return false
69
+ const firstParam = fn.params[0]
70
+ if (!firstParam || firstParam.type !== 'Identifier') return false
71
+ const setTimeoutCall = extractSingleCallExpression(fn.body)
72
+ if (!setTimeoutCall) return false
73
+ if (setTimeoutCall.callee?.type !== 'Identifier' || setTimeoutCall.callee.name !== 'setTimeout') return false
74
+ if (!Array.isArray(setTimeoutCall.arguments) || setTimeoutCall.arguments.length < 1) return false
75
+ return isBareResolveCallback(setTimeoutCall.arguments[0], firstParam.name)
76
+ }
77
+
78
+ /**
79
+ * Простий рекурсивний обхід AST: заходимо в усі об'єкти/масиви, щоб знайти `NewExpression`.
80
+ * @param {unknown} node корінь або під-вузол AST
81
+ * @param {(n: Record<string, unknown>) => void} visit виклик для кожного об'єкта-вузла з `type`
82
+ * @returns {void}
83
+ */
84
+ function walkAst(node, visit) {
85
+ if (!node || typeof node !== 'object') return
86
+ if (Array.isArray(node)) {
87
+ for (const item of node) walkAst(item, visit)
88
+ return
89
+ }
90
+ if (typeof node.type === 'string') {
91
+ visit(node)
92
+ }
93
+ for (const key of Object.keys(node)) {
94
+ if (key === 'parent') continue
95
+ const v = node[key]
96
+ if (v && typeof v === 'object') walkAst(v, visit)
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Знаходить усі `new Promise(resolve => setTimeout(resolve, ms))` у тексті.
102
+ * @param {string} content вихідний код
103
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
104
+ * @returns {{ line: number, snippet: string }[]} список порушень
105
+ */
106
+ export function findPromiseSetTimeoutInText(content, virtualPath = 'scan.ts') {
107
+ const program = parseProgramOrNull(content, virtualPath)
108
+ if (!program) return []
109
+ /** @type {{ line: number, snippet: string }[]} */
110
+ const out = []
111
+ walkAst(program, node => {
112
+ if (!isPromiseSetTimeoutDelay(node)) return
113
+ out.push({
114
+ line: offsetToLine(content, node.start),
115
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
116
+ })
117
+ })
118
+ return out
119
+ }
120
+
121
+ /**
122
+ * Чи сканувати цей файл за розширенням (JS/TS-сім'я, виключно з `.d.ts`).
123
+ * @param {string} relativePath відносний шлях до файлу
124
+ * @returns {boolean} `true`, якщо розширення підходить для сканування
125
+ */
126
+ export function isPromiseSetTimeoutScanSourceFile(relativePath) {
127
+ if (!SOURCE_FILE_RE.test(relativePath)) return false
128
+ return !relativePath.endsWith('.d.ts')
129
+ }