# lint.mjs

## Огляд

Модуль `lint.mjs` — це «quick-крок» (швидкий етап) правила `style-lint`, який запускає `stylelint --fix` по стильових файлах проєкту: `*.css`, `*.scss`, `*.vue`. Модуль виконує дві окремі задачі:

1. Фільтрує довільний список шляхів, лишаючи з нього тільки файли зі стильовими розширеннями (`.css`, `.scss`, `.vue`).
2. Запускає виконуваний бінарник `stylelint` через `npx` з прапорцем `--fix` — або по точному списку файлів (режим «quick»), або по всьому glob-патерну `**/*.{css,scss,vue}` (режим «ci»).

Модуль написаний як ES-модуль (`.mjs`), не має побічних ефектів на рівні імпорту (не запускає `stylelint` під час завантаження модуля) і експортує дві функції: `filterStyleFiles` та `lint`.

Призначений для використання в інфраструктурі лінтінгу монорепо: викликається з оркестратора правил (наприклад, `tooling.mjs` сусіднього модуля), який передає або конкретний набір змінених файлів (швидкий цикл локального розробника), або `undefined` (повний прогін в CI).

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

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

| Експорт            | Тип        | Призначення                                                                |
| ------------------ | ---------- | -------------------------------------------------------------------------- |
| `filterStyleFiles` | `function` | Чистий фільтр: з масиву шляхів повертає тільки `.css` / `.scss` / `.vue`.  |
| `lint`             | `function` | Запускає `stylelint --fix` через `npx`, повертає `Promise<number>` (exit). |

Дефолтного експорту немає.

Окрім експортів, у модулі визначена константа модульного рівня `STYLE_EXT_RE` — приватна (не експортується) регулярка `/\.(?:css|scss|vue)$/u`, яка матчить лише суфікс імені файлу.

## Функції

### `filterStyleFiles(files)`

**Сигнатура.**

```js
export function filterStyleFiles(files: string[]): string[]
```

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

- `files` — масив рядків-шляхів (відносних або абсолютних — функція не розрізняє). Жодних обмежень на роздільник шляху чи на регістр розширення регулярка не накладає, окрім того що розширення має бути в нижньому регістрі (`.css`, `.scss`, `.vue`).

**Повертає.**

- `string[]` — новий масив, що містить лише ті елементи `files`, у яких суфікс задовольняє регулярку `STYLE_EXT_RE` (`.css` / `.scss` / `.vue` в кінці рядка). Порядок збережено.

**Side effects.**

- Немає. Функція чиста: не змінює вхідний масив, не звертається до файлової системи, не пише в stdout/stderr, не залежить від процесового стану. Імпорт `node:child_process` тут не задіяний.

**Поведінкові деталі.**

- Якщо `files` — порожній масив, повертається порожній масив.
- Якщо в `files` немає жодного стильового файлу, повертається порожній масив.
- Файли з розширеннями `.CSS`, `.Vue` тощо (з великими літерами) **не** будуть включені, бо регулярка не має прапора `i`.
- Регулярка прив'язана до кінця рядка (`$`), тому шляхи на кшталт `foo.css.bak` не пройдуть.
- Прапор `u` (Unicode) у регулярці дозволяє коректно обробляти шляхи з Unicode-символами.

### `lint(files, cwd = process.cwd())`

**Сигнатура.**

```js
export function lint(
  files: string[] | undefined,
  cwd?: string
): Promise<number>
```

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

- `files` — або масив шляхів (тоді функція працює в режимі «quick»: фільтрує його через `filterStyleFiles` і запускає `stylelint` тільки по відфільтрованих файлах), або `undefined` (режим «ci»: запускає `stylelint` по glob-патерну `**/*.{css,scss,vue}` від кореня `cwd`).
- `cwd` — корінь, у якому запускатиметься `npx`. За замовчуванням — поточна робоча директорія процесу (`process.cwd()`).

**Повертає.**

- `Promise<number>` — обіцянка з exit-кодом:
  - `0` — якщо `files` — масив, але після фільтрації не лишилось жодного стильового файлу (швидкий вихід без запуску `stylelint`).
  - Числовий exit-код процесу `npx stylelint ...`, отриманий з поля `r.status` синхронного результату `spawnSync`.
  - `1` — якщо `r.status` не є числом (наприклад, процес був завершений сигналом, не зміг запуститися або інша «нештатна» ситуація).
- Хоча всередині немає `async` й `await`, повертається явно `Promise.resolve(...)`, тож сигнатура асинхронна і викликати функцію можна через `await`.

**Side effects.**

- Запускає **зовнішній процес** `npx stylelint --fix ...` через `spawnSync` з `stdio: 'inherit'`. Це означає:
  - `stdout`, `stderr`, `stdin` дочірнього процесу наслідуються від батьківського — весь вивід `stylelint` йде безпосередньо в консоль користувача (або в логи CI).
  - Функція **блокує** event-loop поточного процесу до завершення `stylelint` (попри асинхронний інтерфейс).
  - `stylelint --fix` **модифікує файли на диску** там, де він здатен автоматично виправити порушення стилю.
- Запис у файлову систему здійснюється самим `stylelint`, а не цим модулем напряму.
- Модуль не намагається інсталювати `stylelint` — він повинен бути доступний через `npx` (зазвичай через `package.json` залежність, локальний `node_modules/.bin` або глобально).

**Поведінкові деталі.**

- Аргументи `npx` будуються в масив `args`: завжди починається з `['stylelint', '--fix']`, далі або додається один елемент-glob `**/*.{css,scss,vue}`, або розпаковується список відфільтрованих файлів через spread.
- Glob-патерн `**/*.{css,scss,vue}` передається без екранування — його **розкриває сам `stylelint`** (а не shell), оскільки `spawnSync` викликається без `shell: true`. Тобто це не shell-glob, а аргумент, який `stylelint` парсить як glob-патерн.
- У режимі «quick» з порожнім результатом фільтрації функція не викликає `npx` взагалі — це оптимізація для випадків, коли в наборі змінених файлів немає жодного стильового файлу (типово для PR, що зачіпає тільки JS/TS-код).

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

### Зовнішні npm-пакети

Прямих імпортів з npm немає.

### Node.js вбудовані модулі

- `node:child_process` — імпорт `{ spawnSync }`. Використовується для синхронного запуску `npx stylelint ...`.

### Зовнішні бінарники / системні залежності

- `npx` — має бути в `PATH`. Це стандартний компаньйон `npm` / `bun` / `pnpm`.
- `stylelint` — має бути запускний через `npx` (через локальний `node_modules` або глобально). Конкретна версія й конфіг (`.stylelintrc*`) знаходяться поза межами цього модуля й керуються рівнем монорепо.

### Внутрішні модульні зв'язки

Сам файл нічого не імпортує з інших файлів проєкту. Натомість його імпортують:

- сусідній `tooling.mjs` (оркестратор правила `style-lint`),
- та/або тести в підтеці `tests/`.

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

### Типовий сценарій 1: quick-режим (локальний розробник)

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

const changed = ['src/components/Button.vue', 'src/utils/dom.ts', 'src/styles/main.scss']

const code = await lint(changed)
process.exit(code)
```

Що відбудеться:

1. `filterStyleFiles(changed)` поверне `['src/components/Button.vue', 'src/styles/main.scss']`.
2. Сформується команда `npx stylelint --fix src/components/Button.vue src/styles/main.scss`.
3. `spawnSync` запустить `npx`, `stylelint` обробить тільки ці два файли, виправить що зможе, лог піде в консоль.
4. Функція поверне promise з exit-кодом `stylelint` (`0` — все добре; ненульовий — лишилися невиправні порушення або помилка конфігу).

### Типовий сценарій 2: ci-режим (повний прогін)

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

const code = await lint(undefined, process.cwd())
process.exit(code)
```

Що відбудеться:

1. `files === undefined`, тому в `args` додається літерал `**/*.{css,scss,vue}`.
2. Сформується команда `npx stylelint --fix '**/*.{css,scss,vue}'` (без shell-escaping — це аргумент).
3. `stylelint` сам розкриває glob від `cwd` і обходить весь проєкт.
4. Функція повертає його exit-код.

### Сценарій 3: quick-режим без жодного стильового файлу

```js
const code = await lint(['README.md', 'src/index.ts'])
// code === 0, npx навіть не запускався
```

Що відбудеться:

1. `filterStyleFiles` поверне `[]`.
2. Гілка `if (style.length === 0) return Promise.resolve(0)` поверне `0` миттєво.
3. Жодного дочірнього процесу не створюється — це швидкий «no-op».

### Як вписується в загальну архітектуру

Цей файл — реалізаційна одиниця «quick» кроку правила `style-lint`. Правило в монорепо має дві площини запуску:

- **quick** (швидкий) — викликається після зміни файлів локально, працює тільки по зміненому списку для мінімального часу відгуку.
- **ci** — викликається в неперервній інтеграції, проганяє по всьому glob-у щоб гарантовано перевірити весь репозиторій.

Файл `lint.mjs` уніфікує обидва режими в одній функції, де перемикач режиму — це `files === undefined` (ci) vs `files: string[]` (quick).

### Контракти й гарантії

- Функція `lint` **синхронно** виконує дочірній процес, але повертає `Promise<number>` — це навмисно, щоб уніфікувати інтерфейс з іншими «крок-функціями» в системі правил, які можуть бути по-справжньому асинхронні. Caller завжди робить `await lint(...)`.
- `lint` не кидає виключень: будь-який збій `npx` (включаючи відсутній бінарник) перетвориться в exit-код `1` через гілку `typeof r.status === 'number' ? r.status : 1`. Це робить функцію передбачуваною для оркестратора, який оперує тільки числовими кодами.
- `filterStyleFiles` — стабільна чиста функція, придатна для незалежного юніт-тестування (саме тому винесена як окремий експорт).

## Rebuild Test

Документація відповідає коду версії на момент написання: 35 рядків, два експорти (`filterStyleFiles`, `lint`), одна приватна регулярка (`STYLE_EXT_RE`), одна імпортована функція з `node:child_process` (`spawnSync`). Якщо файл буде переписано (наприклад, додасться третій експорт, зміниться сигнатура `lint`, з'явиться нова гілка для `--config`, або `spawnSync` буде замінено на асинхронний `spawn`), цей документ потребує перегенерації.
