# itube-specs

Общий **Nuxt Layer** для сайтов itube. Держит единую базу: компоненты, composables, сервисы, утилиты,
runtime-хелперы, типы, lib-данные, middleware, SSR-плагины, BFF-хендлеры (`server/api`) и **весь SCSS —
палитру, темы, миксины и глобальные стили**.
Приложения расширяют слой (`extends: ['itube-specs']`) и форкаются под разные сайты одной ниши —
поэтому слой должен оставаться **самодостаточным и site-agnostic**.

> Bitbucket: https://bitbucket.org/luckytube/specs/src/master/
>
> Заводишь новый сайт поверх слоя — [**NEW_SITE_CHECKLIST.md**](./NEW_SITE_CHECKLIST.md).

---

## Стек

- **Nuxt 3** + **Vue 3.5**, TypeScript
- **@nuxt/icon**, **@nuxtjs/i18n**, **@nuxt/eslint** — как модули слоя
- **ESLint** + **Stylelint** (гейтят на ноль варнингов), **Vitest** (юнит)
- Публикуется в npm; приложения ставят опубликованную версию

**Требования:** Node `>=22.12`.

---

## Как работать со слоем

Слой публикуется в npm, прод/CI собираются с **опубликованной** копии (`node_modules/itube-specs`).
Локально приложение подсасывает слой напрямую через симлинк — правки видны сразу, без публикации.

### Локальная разработка (авто-линк из приложения)

В приложении `npm run dev` перед стартом (хук `predev`) вешает симлинк
`node_modules/itube-specs` → `LAYER_PATH` (путь берётся из `.env` приложения). Дальше:

1. Правим слой прямо здесь (по `LAYER_PATH`) — изменения видны на локалке приложения сразу (HMR),
   cmd+click по сущностям слоя ведёт в эти исходники.
2. Правки готовы: коммит в слое → `npm run patch` → пуш → мерж → публикация.
3. В приложении: `npm run spec` (`npm install itube-specs@latest`) тянет опубликованную версию
   (симлинк заменяется реальным пакетом), коммит, пуш на прод.

`npm run dev:published` в приложении — дев против опубликованной версии, минуя авто-линк.

> **npm install и другие команды, меняющие зависимости, в этом репозитории не запускаем** — зависимости
> и публикацию ведёт мейнтейнер вручную. Править `package.json` можно, ставить пакеты — нет.

### Проектные оверрайды

Приоритет авто-импорта в приложении: проектные `components/`/`composables/` > слой.

Приложение может завести файл с **тем же именем**, что сущность слоя (напр. `<UiBtn>`), в своих
`components/`/`composables/` — победит проектный (он выше слоя). Так форк меняет вёрстку/логику под себя
**без переименования**. Осмысленно: если разницу решает пропс — делать пропсом, а не форкать весь компонент.

---

## Команды

```bash
npm run lint         # ESLint (гейтит на ноль варнингов)
npm run lint:fix     # ESLint --fix
npm run lint:css     # Stylelint по **/*.{scss,vue}
npm run lint:css:fix # Stylelint --fix
npm run check:themes # сверяет наборы имён токенов во всех темах
npm run check:styles # компилирует стили каждого SFC каждой темой (~1.5s)
npm run check:breakpoints # сверяет JS-зеркало брейкпоинтов с $brkpnts в SCSS
npm run test         # vitest однократно
npm run patch        # npm version patch — бамп версии перед публикацией
```

Pre-commit (husky + lint-staged): на закоммиченных файлах прогоняются `eslint --fix` и `stylelint --fix`.
Если в коммите есть `assets/scss/themes/**`, гоняется `check:themes`; если любой `.vue` или
`assets/scss/**` — `check:styles`; если пара файлов брейкпоинтов — `check:breakpoints`. Все три есть и в
CI, в шаге Lint.
⚠️ **Шаблоны `.vue` не типизируются вообще.** Обычный `tsc` их не читает, а `vue-tsc` не подключён (накопился
бэклог ошибок в шаблонах, подключение — отдельная задача). Значит чистый `tsc` ничего не говорит про выражения
в шаблоне: арифметика над строковым union, неверный тип пропа, переименованное поле пройдут молча.

---

## Структура

```
components/    # общие компоненты (pathPrefix: имя = папка + файл)
composables/   # автоимпортируемые composables (+ fetch/, __tests__/)
services/      # синглтон-клиенты внешнего API (services/api/*) + SiteDataService
utils/         # доменные утилиты (file = function, co-located __tests__/)
runtime/       # runtime-значения и хелперы (barrel index.ts)
  constants/   # доменные `as const` (Niche, PlaylistType, AdSpotType, …)
  utils/       # чистые хелперы, cleaners/, converters/
types/         # .d.ts типы (barrel index.d.ts — с расширением .d.ts в реэкспортах)
lib/           # продуктовые статические данные (barrel index.ts): scheme/*, *-items, *-scheme
config/        # build-time конфиг для приложения (barrel index.ts): themes, css-breakpoints
middleware/    # глобальные: auth.global, normalize.global, age.global
plugins/       # SSR/клиент: country/countries/mobile (SSR), services.client, sentry.client, adv
server/        # BFF: api/** (тонкие прокси к внешнему API), utils/metrics, plugins/*, tasks
assets/
  icons/       # спрайты/svg для <UiIcon>
  scss/
    vars/      # _palette (сырые $color-*), _vars (скаляры), _tokens (CSS custom props), _z-index, _index
    themes/    # по папке на тему: light/_theme.scss, dark/_theme.scss — токены по компонентам
    mixins/    # font-sizes, breakpoints, accessibility, page, video-card, surface, button
    base/      # document, global, hacks, animations, loading
    layout/    # layout, page
    main.scss  # точка входа: vars/tokens + base + layout
```

> `middleware`/`plugins`/`server` и каждая подпапка `assets/scss/*` обязаны быть в `files` (whitelist) —
> иначе не попадут в паблиш (см. раздел `files`/`exports`).

---

## Темы

Темы живут здесь, по папке на тему, и раздаются всем сайтам:

```
assets/scss/themes/light/_theme.scss
assets/scss/themes/dark/_theme.scss
```

Приложение только **выбирает** одну — `theme:` в его `config/site.config.ts`. Оттуда значение читают
и `app.config.ts` (в рантайм), и `nuxt.config.ts`, который добавляет `themes/<theme>/` в Sass
`loadPaths`. `@forward 'theme'` в `vars/_index.scss` резолвится туда, потому что рядом с ним
`_theme.scss` нет.

**Папка-на-тему обязательна:** Sass требует литеральный путь в `@use`/`@forward`, значит имя файла
всегда `_theme.scss`, а варьироваться может только папка. Плоские `themes/light.scss` потребовали бы
`@forward 'themes/light'` — литерал, который нельзя подставить на билде.

Приоритет `loadPaths` — приложение первым, поэтому сайт может перекрыть любой партиал по имени:
свой `assets/scss/vars/_theme.scss` в проекте выиграет у темы слоя (относительный резолв Sass старше
`loadPaths`). Это точка пер-сайтовой темизации.

**Все токены обязаны быть во всех темах.** Отсутствующий токен — жёсткая ошибка Sass в момент, когда
тема выбрана, то есть падает вся сборка сайта, а не «поехал цвет». Это гейтит `npm run check:themes`
(pre-commit при правке тем + CI). Сверяются только верхнеуровневые `$name:`, поэтому начинка
`$surfaces` / `$button-variant-overrides` игнорируется — она по замыслу разная.

Паритет доказывает лишь то, что темы согласованы **между собой**. Токен, использованный в компоненте, но не
заведённый **ни в одной** теме, эту проверку пройдёт и всё равно уронит сборку — для этого есть
`npm run check:styles`: он достаёт каждый `<style lang="scss">`, приписывает тот же `@use`-префикс, что
подставляет приложение, и компилирует по разу на тему. То же, что делает Vite, — значит ловит и неизвестные
миксины, и неверные аргументы `@include`, и показывает, в какой теме падает.

Контекстные различия (контрол внутри попапа, вариант кнопки, зависящий от темы) решают миксины
`surface()` и `button-variant()` из `mixins/`: место объявляет себя, а значения даёт тема через карты
`$surfaces` / `$button-variant-overrides`. Компоненты про темы не знают. `appConfig.theme` — только
для структурных различий (`v-if`, другой элемент), никогда для цвета.

---

## Импорты

Внутри слоя — **относительные пути** (`../../runtime`, `../types`, `../../services/api/...`), никогда не
через имя пакета. Приложения импортируют как `itube-specs/runtime`, `itube-specs/services/...`,
`itube-specs/utils/...`, а типы — `itube-specs/types`.

Алиасы `~/…`/`@/…` в файле слоя резолвятся на **приложение**, не на слой — в коде слоя их не используем.
Сейчас таких app-зависимостей в слое не осталось: то, что раньше приходило из приложения (фича-флаги,
ad-конфиг), теперь читается из `runtimeConfig.public` (значения задаёт приложение).

### `files` / `exports`

`package.json` определяет, что публикуется и как резолвится. Добавляя новую top-level папку, которую
импортируют приложения:
- добавь её в **`files`** (иначе код не попадёт в пакет);
- добавь **`exports`**: barrel'ы (`types`/`runtime`/`lib`) — явные index-энтри; extensionless-сабпасы
  (`services`/`composables`/`utils`) — паттерн с `.ts` (`"./services/*": "./services/*.ts"`); компоненты
  импортятся с `.vue` и идут через `"./*": "./*"`.

Типы экспортируются из `types/index.d.ts` — **обязательно с расширением `.d.ts`** в реэкспортах.

---

## Что живёт в слое, а что в приложении

- **Слой:** компоненты, composables, API-сервисы, доменные утилиты, runtime-константы/хелперы, типы,
  `lib/` (продуктовые данные), `config/` (темы, брейкпоинты), middleware (`auth`/`normalize`/`age`), SSR-плагины
  (`country`/`mobile`/`services`/`sentry`/`adv`), BFF-хендлеры `server/api/**` — всё site-agnostic.
- **Приложение:** только per-site — **значения** config (флаги/реклама/локали) через `runtimeConfig.public`
  (слой держит пустые контейнеры-заглушки), i18n-локали, `public/` и **выбор темы** (`theme:` в
  `config/site.config.ts`). Своего SCSS у приложения нет: `assets/scss/` — пустой слот-оверрайд.
  `server/` целиком в слое, включая locale-кластер.

---

## Конвенции

Полные правила — в [**CLAUDE.md**](./CLAUDE.md). Кратко:

- **Компоненты** — `pathPrefix: true`: имя = папка + файл (`ui/icon.vue` → `<UiIcon>`), BEM-класс = kebab
  тега. Корень папки — `index.vue` (или `main.vue`, если иначе имя было бы односложным).
- **`as const`** вместо `enum` — доменные наборы в `runtime/constants/`, тип через
  `typeof X[keyof typeof X]`, значения строковые. Enum остаётся в бандле, в опубликованном пакете
  `import type` на нём ломается в рантайме, а там, где значение приходит из API/URL/куки, сырую строку
  в него не положить. Фреймворковые union'ы не оборачивать. Подробнее — в [CLAUDE.md](./CLAUDE.md).
- **Стили** — co-location в `<style lang="scss">` без `scoped`, flat-BEM, только дизайн-токены.
  `vars` и `mixins` **авто-инжектятся** через `additionalData` приложения, поэтому в блоке компонента
  `@use` писать не надо. Сам `sass` в слое не установлен — SCSS компилируется внутри приложения, но
  все исходники (палитра, темы, миксины) лежат здесь.
- **Разметка** — семантические теги, `<div>` только когда ничего не подходит.
- **TS/JS** — всегда точка с запятой, без лишних комментариев.

---

## Тесты

Vitest через `@nuxt/test-utils`. В слое — **только тесты без app-специфичного окружения** (чистая логика
composables/utils/runtime + composable-тесты в nuxt-env без монтирования SFC). Компонентные тесты
(`mountSuspended`) живут в **приложении**: в слое подделан i18n (авто-импорты `useI18n`/`useLocalePath` от
`@nuxtjs/i18n` флапают недетерминированно) и нет SCSS-инъекции токенов. Значения `runtimeConfig`
(`featureFlags`/`adsConfig`) в слое — пустые контейнеры, поэтому его код при бутстрапе в CI не падает,
но реальные значения приходят только из приложения.
