# lint.mjs

## Огляд

Модуль `lint.mjs` реалізує **quick-крок** правила `js-lint`: послідовний запуск двох лінтерів — `oxlint` та `eslint` — обох із прапорцем автофіксу (`--fix`). Модуль безпосередньо викликається lint-оркестратором проєкту (зокрема командами `n-cursor lint` для quick-фази та `lint-ci` для CI-фази).

Поведінка має два режими, які перемикаються типом вхідного параметра `files`:

- **Quick-режим** (`files` — масив рядків): із переданого списку відбираються лише js-подібні файли за розширенням; якщо таких немає — крок завершується успішно без запуску інструментів; інакше `oxlint`/`eslint` отримують точковий перелік цих файлів.
- **CI-режим / повний прогін** (`files === undefined`): `oxlint` викликається без шляхів (бінар сам обходить корінь за своєю конфігурацією), `eslint` отримує явну ціль `.` (поточна директорія).

Крос-файлові перевірки (наприклад `jscpd`, `knip`) у цей модуль **не входять** — вони реалізовані в окремому правилі `js-lint-ci` і виконуються виключно у CI-фазі.

Запуск зовнішніх бінарів робиться через `bunx` (з тулчейну Bun) і успадковує stdio батьківського процесу, тому увесь stdout/stderr лінтерів видно у консолі викликача в реальному часі. Семантика виходу: при ненульовому коді `oxlint` модуль одразу повертає його — `eslint` у такому випадку **не** запускається (fail-fast). Якщо `oxlint` успішний — повертається код виходу `eslint`.

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

Модуль є ES-модулем (`.mjs`) із двома іменованими експортами:

| Експорт         | Тип                                                               | Призначення                                                   |
| --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
| `filterJsFiles` | `(files: string[]) => string[]`                                   | Фільтрує список шляхів, залишаючи лише js-подібні розширення. |
| `lint`          | `(files: string[] \| undefined, cwd?: string) => Promise<number>` | Точка входу quick-кроку: запускає oxlint+eslint з автофіксом. |

Default-експорту немає. Внутрішня функція `run` приватна та не реекспортується.

## Функції

### `filterJsFiles(files)`

**Сигнатура:** `filterJsFiles(files: string[]): string[]`

**Параметри:**

- `files` — масив рядків зі шляхами файлів (відносними або абсолютними; модуль шляхи не нормалізує).

**Повертає:** новий масив, що містить лише ті елементи `files`, чий шлях завершується одним із розширень:
`.mjs`, `.cjs`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`.

Перевірка виконується регулярним виразом `JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u` — match за кінцем рядка, регістрозалежний, з прапорцем `u` (Unicode). Порядок та дублікати оригінального масиву зберігаються; вхідний масив не мутується (використовується `Array.prototype.filter`).

**Side effects:** немає (pure-функція).

**Граничні випадки:**

- Порожній масив → порожній масив.
- Файл без розширення або з не-js розширенням → відкидається.
- `.d.ts` — формально матчиться (хвіст `.ts`), тож потрапить у вихід; модуль не виключає декларацій типів спеціально.
- Шлях із крапкою у директорії, але без js-розширення наприкінці (наприклад `a.b/c.txt`) → не матчиться (через якір `$`).

### `run(args, cwd)` _(приватна)_

**Сигнатура:** `run(args: string[], cwd: string): number`

**Параметри:**

- `args` — масив аргументів, що передаються `bunx` (перший елемент — назва бінаря лінтера, далі його аргументи/таргети).
- `cwd` — робоча директорія для дочірнього процесу.

**Повертає:** числовий exit code. Якщо `spawnSync` повернув `r.status` числом — повертається саме він; інакше (наприклад процес завершився сигналом і `status === null`) повертається `1`.

**Side effects:**

- Синхронно (блокуюче) запускає дочірній процес `bunx <args>` через `node:child_process.spawnSync`.
- Успадковує stdio батьківського процесу (`stdio: 'inherit'`) — тобто весь вивід лінтера йде напряму в термінал/лог поточного процесу.
- Може записувати файли на диск опосередковано: лінтери викликаються з `--fix` і модифікують вихідний код.

**Помилки запуску:** `spawnSync` не кидає виняток за нульового статусу запуску, але у разі помилки спавна повертає об’єкт із `error` та `status === null` — у цьому випадку функція повертає `1` (м’яка інтерпретація як «не OK»).

### `lint(files, cwd?)`

**Сигнатура:** `lint(files: string[] | undefined, cwd: string = process.cwd()): Promise<number>`

**Параметри:**

- `files`:
  - `string[]` — quick-режим: лінтити **лише** ці файли (після фільтрації js-розширень).
  - `undefined` — режим повного прогону: лінтити **весь** проєкт.
- `cwd` — корінь, у якому виконуються `bunx oxlint` та `bunx eslint`. За замовчуванням — `process.cwd()`.

**Повертає:** `Promise<number>`. Резолвиться зі значенням exit code:

- `0` — обидва лінтери (або один із них у спеціальному випадку) завершилися успішно.
- `≠0` — є порушення або помилка запуску; повертається код того лінтера, що першим завершився ненульовим (oxlint має пріоритет).

**Логіка побудови аргументів:**

1. Стартові значення: `oxArgs = ['oxlint', '--fix']`, `esArgs = ['eslint', '--fix']`.
2. Якщо `files === undefined`:
   - `oxArgs` лишаються без явних таргетів (oxlint обходить проєкт сам).
   - До `esArgs` додається `.` → стає `['eslint', '--fix', '.']`.
3. Інакше (масив):
   - Фільтрація через `filterJsFiles(files)`.
   - Якщо фільтрований список порожній — функція **одразу** повертає `Promise.resolve(0)` (немає чого лінтити; oxlint/eslint **не** запускаються).
   - Інакше: `oxArgs = ['oxlint', '--fix', ...js]`, `esArgs = ['eslint', '--fix', ...js]`.

**Послідовність виконання:**

1. Синхронно запускається `oxlint` через `run(oxArgs, cwd)` → отримуємо `ox`.
2. Якщо `ox !== 0` → негайно повертається `Promise.resolve(ox)` (eslint **не** запускається — fail-fast).
3. Якщо `ox === 0` → синхронно запускається `eslint` через `run(esArgs, cwd)`; результат загорнуто у `Promise.resolve(...)` і повернуто.

**Тип повертаного значення:** функція синхронна по суті (внутрішні `spawnSync` блокують event-loop), але інтерфейс — `Promise<number>`, що дозволяє оркестратору однаково обробляти async- та sync-кроки.

**Side effects:**

- Спавнить до двох дочірніх процесів (`bunx oxlint`, `bunx eslint`).
- Через прапорець `--fix` лінтери модифікують вихідні файли на диску (автофікс).
- Виводить stdout/stderr лінтерів напряму в консоль батьківського процесу.

**Граничні випадки:**

- `files = []` → після фільтрації порожній список → resolve(0), запусків немає.
- `files = ['README.md']` → після фільтрації порожній список → resolve(0), запусків немає.
- `files = undefined` + порожній проєкт → oxlint/eslint самі обходять корінь і повертають свій код (часто 0).
- Невказаний `cwd` → `process.cwd()` на момент виклику (а не на момент імпорту).

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

### Зовнішні (рантайм)

| Залежність                     | Звідки                        | Призначення                                                                                 |
| ------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------- |
| `node:child_process.spawnSync` | Стандартна бібліотека Node.js | Синхронний запуск дочірніх процесів `bunx oxlint` / `bunx eslint` зі спадкуванням stdio.    |
| `bunx`                         | Тулчейн Bun (PATH)            | Резолвер/раннер бінарів пакетів — використовується для виклику `oxlint` та `eslint`.        |
| `oxlint`                       | Доступний через `bunx`        | Швидкий лінтер на Rust; перший етап quick-кроку, виконується з `--fix`.                     |
| `eslint`                       | Доступний через `bunx`        | Основний лінтер на JavaScript; другий етап, виконується з `--fix` після успішного `oxlint`. |

### Внутрішні

Модуль **не** імпортує локальних модулів проєкту — повністю автономний у межах файлу. Контекст використання задає зовнішній lint-оркестратор (правило `js-lint`), який передає аргументи `files`/`cwd`.

### Споживачі

Викликається lint-оркестратором правил `js-lint` (quick-фаза). Модуль `lint-ci` (повний CI) реалізований окремо і додає крос-файлові інструменти (`jscpd`, `knip`) — у поточному файлі їх немає.

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

### Загальний потік (псевдо-діаграма)

```
викликач (n-cursor lint / lint-ci)
        |
        v
   lint(files, cwd)
        |
   files === undefined ? --- так ---> oxArgs=['oxlint','--fix']
        |                              esArgs=['eslint','--fix','.']
        | ні
        v
   js = filterJsFiles(files)
        |
   js.length === 0 ? --- так ---> Promise.resolve(0)  (вихід)
        | ні
        v
   oxArgs=['oxlint','--fix',...js]
   esArgs=['eslint','--fix',...js]
        |
        v
   ox = run(oxArgs, cwd)   // bunx oxlint ...
        |
   ox !== 0 ? --- так ---> Promise.resolve(ox)  (eslint не запускається)
        | ні
        v
   es = run(esArgs, cwd)   // bunx eslint ...
        |
        v
   Promise.resolve(es)
```

### Приклад: quick-режим (масив файлів)

```js
import { lint } from './lint.mjs'

const changed = [
  'src/foo.ts',
  'src/bar.vue',
  'README.md', // буде відфільтровано
  'scripts/baz.mjs'
]

const code = await lint(changed)
// 1) filterJsFiles → ['src/foo.ts','src/bar.vue','scripts/baz.mjs']
// 2) bunx oxlint --fix src/foo.ts src/bar.vue scripts/baz.mjs
// 3) якщо ox === 0 → bunx eslint --fix src/foo.ts src/bar.vue scripts/baz.mjs
process.exit(code)
```

### Приклад: CI-режим (повний прогін)

```js
import { lint } from './lint.mjs'

const code = await lint(undefined, '/path/to/repo')
// 1) bunx oxlint --fix             (oxlint сам обходить проєкт)
// 2) якщо ox === 0 → bunx eslint --fix .
process.exit(code)
```

### Приклад: масив без js-файлів

```js
import { lint } from './lint.mjs'

const code = await lint(['CHANGELOG.md', 'docs/spec.md'])
// filterJsFiles → []  → Promise.resolve(0)
// жодного процесу не спавниться
```

### Семантика кодів виходу

- `0` — обидва лінтери успішні **або** немає js-файлів для лінтингу в quick-режимі.
- `ox` (≠0) — `oxlint` знайшов порушення / завершився помилкою; `eslint` пропущено.
- exit code `eslint` — повертається, якщо `oxlint` був успішний, а `eslint` — ні.
- `1` (з `run`) — фолбек, коли `spawnSync` не повернув числового `status` (наприклад, процес убитий сигналом).

### Важливі зауваги для викликачів

- Виклики `oxlint` та `eslint` **синхронні** (через `spawnSync`) — `Promise<number>` тут лише обгортка для уніфікації API; функція **блокує** event-loop на час лінтингу.
- Завдяки `stdio: 'inherit'` весь вивід лінтерів видно одразу — додаткове логування зайве.
- `--fix` модифікує файли на диску; якщо потрібно «чисто перевірити без правок» — цей модуль такого режиму не пропонує, доведеться обійти його або змінити аргументи.
- За правилами проєкту (див. `n-js-lint` / `n-lint`) **заборонені** паралельні запуски `eslint` — оркестратор має викликати `lint` **послідовно**, без фонових/підзадачних дублів.
- `oxlint` має пріоритет (fail-fast): якщо він валиться — `eslint` не запускається, тож звіт про помилки у тій самій сесії покаже лише результати oxlint.

## Rebuild Test

Описане в цій документації достатньо для відтворення модуля з нуля:

1. ES-модуль `.mjs` без default-експорту.
2. Імпорт `spawnSync` з `node:child_process`.
3. Константа `JS_EXT_RE` — регекс `^.*\.(mjs|cjs|js|jsx|ts|tsx|vue)$` (формально: `/\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u`).
4. Експорт `filterJsFiles(files)`: `return files.filter(f => JS_EXT_RE.test(f))`.
5. Приватна `run(args, cwd)`: `spawnSync('bunx', args, { cwd, stdio: 'inherit' })`; повертає `r.status` якщо це `number`, інакше `1`.
6. Експорт `lint(files, cwd = process.cwd())`:
   - стартові аргументи: `oxArgs=['oxlint','--fix']`, `esArgs=['eslint','--fix']`;
   - якщо `files === undefined` → `esArgs.push('.')`;
   - інакше: `js = filterJsFiles(files)`; якщо `js.length === 0` → `Promise.resolve(0)`;
     інакше переписати: `oxArgs=['oxlint','--fix',...js]`, `esArgs=['eslint','--fix',...js]`;
   - `ox = run(oxArgs, cwd)`; якщо `ox !== 0` → `Promise.resolve(ox)`;
   - інакше → `Promise.resolve(run(esArgs, cwd))`.

Поведінкові інваріанти для верифікації:

- `filterJsFiles([])` ⇒ `[]`.
- `filterJsFiles(['a.md','b.ts','c.vue','d.txt'])` ⇒ `['b.ts','c.vue']`.
- `await lint([])` ⇒ `0`, без побічних спавнів.
- `await lint(['x.md'])` ⇒ `0`, без побічних спавнів.
- `await lint(undefined)` ⇒ запускає `bunx oxlint --fix`, далі (якщо OK) `bunx eslint --fix .`.
- При ненульовому коді `oxlint` `eslint` не запускається.
