# `bunyan-imports.mjs`

## Огляд

Модуль `bunyan-imports.mjs` — це AST-сканер, який знаходить у вихідних файлах JavaScript / TypeScript заборонені імпорти й виклики модулів `@nitra/bunyan` та застарілого `bunyan`. Згідно з правилом `js-run.mdc` уся логіка структурованого логування у проєкті переведена на `@nitra/pino`, тому будь-яке згадування `bunyan`/`@nitra/bunyan` має бути замінене на `@nitra/pino`.

Ключові властивості:

- Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу; це робить сканер стійким до коментарів, рядкових літералів та форматувань.
- На додачу до статичних `import`-ів сканер обходить AST програми і ловить:
  - `require('@nitra/bunyan')` / `require('bunyan')` — для CommonJS;
  - динамічний `import('@nitra/bunyan')` / `import('bunyan')` — для випадків асинхронного підключення в межах одного файлу.
- Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках парсера повертається **порожній** результат — спершу слід полагодити синтаксис, потім перезапустити перевірку.
- Декларації типів (`*.d.ts`) виключені з обходу через окремий хелпер.

Файл є частиною бібліотеки правила `js-run` й використовується чек-скриптами цього правила для сигналізації про порушення.

## Експорти / API

Модуль експортує три іменовані функції:

| Експорт                                          | Призначення                                                 |
| ------------------------------------------------ | ----------------------------------------------------------- |
| `findBunyanImportsInText(content, virtualPath?)` | Основний AST-сканер тексту файлу — повертає масив порушень. |
| `isBunyanScanSourceFile(relativePath)`           | Фільтр за розширенням файлу (JS/TS-сім'я).                  |
| `shouldSkipFileForBunyanScan(relativePosix)`     | Виключення декларацій типів (`*.d.ts`).                     |

Внутрішні (не експортовані) константи:

- `SOURCE_FILE_RE` — `/\.([cm]?[jt]sx?)$/u` — регулярний вираз для розширень `.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts`, `.tsx`.
- `FORBIDDEN_MODULES` — `Set` з двох імен модулів: `'@nitra/bunyan'` і `'bunyan'`.

## Функції

### `findBunyanImportsInText(content, virtualPath = 'scan.ts')`

Основний сканер. Парсить переданий текст як модуль JS/TS і збирає всі місця, де згадуються заборонені модулі через статичний `import`, `require(...)` або динамічний `import(...)`.

- Сигнатура: `findBunyanImportsInText(content: string, virtualPath?: string) => { line: number, snippet: string, module: string }[]`
- Параметри:
  - `content` (`string`) — повний вихідний код, який треба перевірити.
  - `virtualPath` (`string`, опційно, за замовчуванням `'scan.ts'`) — віртуальний шлях, який передається в парсер; використовується тільки для вибору `lang` (TS/JS/JSX/TSX) через `langFromPath(...)`. Реальний файл на диску не читається.
- Повертає: масив об'єктів-порушень. Кожен елемент:
  - `line` — номер рядка (1-індекс) місця початку конструкції (`offsetToLine`).
  - `snippet` — нормалізований текстовий зріз `content.slice(start, end)` через `normalizeSnippet` (стискання пробілів/обрізання).
  - `module` — рядок самого імпортованого імені (`'@nitra/bunyan'` або `'bunyan'`).
- Порядок результату: спершу всі статичні `import`-и в порядку їх появи в `module.staticImports`, далі — `require`/`import(...)` у порядку обходу AST через `walkAstWithAncestors`.
- Side effects: відсутні. Функція суто функціональна: жодних I/O, мутацій вхідних параметрів чи логів. Виняткова поведінка `parseSync` поглинається `try/catch`, помилки парсера в `result.errors` трактуються як «нічого не знайдено» — повертається `[]`.
- Граничні випадки:
  - `virtualPath` falsy (порожній рядок, `undefined`) — використовується запасне значення `'scan.ts'`.
  - Якщо `parseSync` кинув виняток або у `result.errors` є записи — повертається `[]`.
  - Якщо `result.module` відсутній (наприклад, скрипт CJS), цикл по `module.staticImports` пропускається через `?? []`, AST-обхід усе одно виконується.
  - Для `require`/динамічного `import` модулі з не-літеральним аргументом (`require(name)`) хелпери з `ast-scan-utils` повернуть `null` і вузол буде проігноровано.

### `isBunyanScanSourceFile(relativePath)`

Швидкий фільтр за розширенням — чи має сенс взагалі парсити цей файл.

- Сигнатура: `isBunyanScanSourceFile(relativePath: string) => boolean`
- Параметри:
  - `relativePath` (`string`) — відносний шлях до файлу (формат розділювача не важливий, перевіряється тільки суфікс).
- Повертає: `true`, якщо розширення входить у JS/TS-сім'ю (`.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts`, `.tsx`); інакше `false`.
- Side effects: відсутні; чиста перевірка регулярним виразом `SOURCE_FILE_RE`.

### `shouldSkipFileForBunyanScan(relativePosix)`

Виключення для декларацій типів TypeScript.

- Сигнатура: `shouldSkipFileForBunyanScan(relativePosix: string) => boolean`
- Параметри:
  - `relativePosix` (`string`) — шлях у posix-форматі (з `/`).
- Повертає: `true`, якщо шлях завершується на `.d.ts` — такий файл містить лише типи й не повинен сканеритися; інакше `false`.
- Side effects: відсутні.
- Примітка: цей хелпер логічно йде паралельно до `isBunyanScanSourceFile`. Файл `*.d.ts` теж задовольнить `SOURCE_FILE_RE`, тож обхідник пакета має застосовувати **обидва** хелпери: спочатку відсіяти за розширенням, потім додатково пропустити `.d.ts`.

## Залежності

Зовнішні (`node_modules`):

- `oxc-parser` — парсер JS/TS, експортує `parseSync`. Сканер користується вузлами `module.staticImports` (метадані статичних `import`-декларацій) та повним AST програми (`result.program`).

Внутрішні (відносний імпорт з `../../../scripts/utils/ast-scan-utils.mjs`):

- `dynamicImportModule(node)` — повертає рядок-аргумент динамічного `import('...')` або `null`.
- `langFromPath(path)` — визначає `lang` (`'ts'`, `'tsx'`, `'js'`, `'jsx'`, …) за розширенням шляху для `parseSync`.
- `normalizeSnippet(text)` — нормалізує сирий шматок коду перед збереженням у результат (типово стискає пробіли / обрізає).
- `offsetToLine(content, offset)` — перетворює числовий offset у тексті на 1-індексований номер рядка.
- `requireCallModule(node)` — повертає рядок-аргумент виклику `require('...')` або `null`.
- `walkAstWithAncestors(program, ancestors, visitor)` — обхід AST з accumulator-предків (тут передається порожній масив; колбек дивиться лише на поточний `node`).

Очікувані виклики (хто використовує цей модуль): чек-скрипти правила `js-run` (наприклад, `npm/rules/js-run/check-*.mjs`), які отримують вміст файлів пакета й передають їх у `findBunyanImportsInText`, після чого формують повідомлення про порушення з `line` / `snippet` / `module`. Обхідник файлів пакета використовує `isBunyanScanSourceFile` для фільтра кандидатів і `shouldSkipFileForBunyanScan` для виключень.

## Потік виконання / Використання

Типовий сценарій інтеграції в чек-правила:

1. Обхідник перебирає всі файли пакета та конвертує шляхи у posix-формат.
2. Для кожного `relativePosix` викликається `isBunyanScanSourceFile(relativePosix)`. Якщо `false` — файл пропускається.
3. Якщо `shouldSkipFileForBunyanScan(relativePosix)` повертає `true` (тобто `*.d.ts`) — файл також пропускається.
4. Інакше файл читається з диска (зовнішнім кодом), а його вміст подається у `findBunyanImportsInText(content, relativePosix)`.
5. Усередині `findBunyanImportsInText`:
   1. Обчислюється `lang` через `langFromPath(virtualPath)`.
   2. Викликається `parseSync(virtualPath, content, { lang, sourceType: 'module' })` всередині `try/catch`.
   3. Якщо парсер кинув виняток або в `result.errors` є записи — повертається `[]`.
   4. Інакше перебираються `result.module?.staticImports ?? []`; для кожного `import`, де `moduleRequest.value` входить у `FORBIDDEN_MODULES`, до результату додається запис `{ line, snippet, module }`.
   5. Далі `walkAstWithAncestors(result.program, [], visitor)` проходить по AST. У відвідувача:
      - `requireCallModule(node)` повертає не-`null` для `require('...')` — якщо модуль заборонений, додаємо запис; інакше пропускаємо.
      - `dynamicImportModule(node)` — те саме для динамічного `import('...')`.
6. Викликач отримує плаский масив порушень і формує звіт (наприклад, друкує `path:line` та `snippet`).

Гарантії та особливості:

- Чисто синхронний прохід — жодного I/O всередині модуля.
- Якщо файл містить синтаксичні помилки, результат `[]` означає **не «все добре»**, а «парсер не зрозумів файл» — це усвідомлене дизайн-рішення: спершу виправити синтаксис, потім перезапустити перевірку.
- Регулярка `SOURCE_FILE_RE` не покриває нестандартні розширення (наприклад, `.vue`, `.svelte`). Якщо для них потрібен власний пайплайн (з вилученням `<script>` блоку), його треба будувати окремо.
- Між списком `module.staticImports` і обходом AST вузли `import` не дублюються: статичні `import`-декларації обходяться лише через `module.staticImports`, а у візиторі перевіряються тільки `require` та динамічний `import(...)`.
- Перелік заборонених модулів суворо обмежений `FORBIDDEN_MODULES` (рівність рядків). Підшляхи на кшталт `@nitra/bunyan/foo` не вловлюються — це навмисна вузька семантика правила.
