itube-specs
| Install | |
|---|---|
npm install itube-specs |
|
| Latest Version: | 0.0.1051 |
| Vue: | 3 |
| Last Updated: | Oct 21, 2026 |
| Links: | npm |
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.
Стек
- 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 приложения). Дальше:
- Правим слой прямо здесь (по
LAYER_PATH) — изменения видны на локалке приложения сразу (HMR), cmd+click по сущностям слоя ведёт в эти исходники. - Правки готовы: коммит в слое →
npm run patch→ пуш → мерж → публикация. - В приложении:
npm run spec(npm install itube-specs@latest) тянет опубликованную версию (симлинк заменяется реальным пакетом), коммит, пуш на прод.
npm run dev:published в приложении — дев против опубликованной версии, минуя авто-линк.
npm install и другие команды, меняющие зависимости, в этом репозитории не запускаем — зависимости и публикацию ведёт мейнтейнер вручную. Править
package.jsonможно, ставить пакеты — нет.
Проектные оверрайды
Приоритет авто-импорта в приложении: проектные components//composables/ > слой.
Приложение может завести файл с тем же именем, что сущность слоя (напр. <UiBtn>), в своих
components//composables/ — победит проектный (он выше слоя). Так форк меняет вёрстку/логику под себя
без переименования. Осмысленно: если разницу решает пропс — делать пропсом, а не форкать весь компонент.
Команды
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 i18n:check # отчёт по общим переводам (не гейт)
npm run i18n:sheet:export # общие локали → i18n/i18n.csv для переводчика
npm run i18n:sheet:import # i18n/i18n.csv → общие локали
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
i18n/locales/ # общие переводы, по файлу на локаль (см. «Переводы»)
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, и показывает, в какой теме падает.
Промежуточного семантического слоя нет — темы ссылаются на палитру напрямую, без тира вида
$text-muted / $role-border-subtle. Сайты форкаются с принципиально разными схемами, и каждому
селектору может понадобиться произвольный цвет, а не одна из горстки ролей. Такой слой либо оброс бы
ролью на селектор (и перестал быть семантическим), либо компоненты начали бы тянуться в обход него.
Имя токена по компоненту, который он обслуживает, держит соответствие честным.
Контекстные различия (контрол внутри попапа, вариант кнопки, зависящий от темы) решают миксины
surface() и button-variant() из mixins/: место объявляет себя, а значения даёт тема через карты
$surfaces / $button-variant-overrides. Компоненты про темы не знают. appConfig.theme — только
для структурных различий (v-if, другой элемент), никогда для цвета.
Переводы
Ключи разделены на два набора, как темы: общая часть здесь, пер-сайтовая в приложении.
Слой, i18n/locales/*.json |
обвязка интерфейса, одинаковая на всех сайтах: навигация, сортировки и фильтры, кнопки-глаголы, пагинация, плеер, авторизация, снекбары, формы репорта и контактов, ошибки, плюрали, длительности, единицы, атрибуты модели |
Приложение, i18n/locales/*.json |
только пер-сайтовое: SEO (meta.*, h1_*, *_footer_text, карты *_footer_links), нишевая лексика и брендовые формулировки, плюс точечные оверрайды общих ключей |
Граница проходит по значению, а не по имени ключа: all_categories («Alphabetical A-Z») — общий,
main_categories («All Gay Porn Categories (A-Z)») — сайтовый, хотя имена похожи.
Как склеивается
@nuxtjs/i18n мержит локали слоёв сам, вручную ничего подключать не надо. Файл слоя попадает в files
локали раньше проектного, значит грузится первым, а проектный ложится поверх. Мерж глубокий, поэтому
приложение может перекрыть один ключ внутри общей группы, не переобъявляя всю группу.
Направление одно: приложение всегда сильнее. Слой не может «дожать» значение — если сайт держит свой ключ, победит он.
Коды локалей выводятся из имён файлов здесь (zh-CN.json → zh), поэтому набор файлов слоя —
канонический. Код, которого нет в LOCALES приложения, всё равно доедет до него лишней локалью, так что
наборы должны совпадать.
Куда добавлять новый ключ
- Строка видна на всех сайтах → сюда, в начало
i18n/locales/en-US.json(«выше — новее»), дальше обычный цикл: коммит →npm run patch→ пуш → мерж и публикация → в приложенииnpm run spec. - Строка про нишу, бренд или SEO → в приложение, слой не трогаем.
⚠️ Не копировать общий ключ в приложение «чтобы был». Дубль с тем же значением ничего не меняет, но
замораживает перевод: правки в слое до этого сайта уже никогда не доедут. i18n:check в приложении зовёт
такое мёртвым оверрайдом и показывает списком.
CSV-мост (работа с переводчиком)
Переводы никогда не правятся руками по локалям — только через CSV. Скрипты живут здесь
(scripts/i18n-sheet.mjs, scripts/i18n-check.mjs), берут пути от cwd и публикуются в пакете, поэтому
один и тот же код обслуживает оба набора: здесь npm run i18n:* работает с общими ключами, в приложении
те же команды зовут копию из node_modules/itube-specs/scripts/ и работают с его собственными. У каждого
набора свой i18n/i18n.csv (в git не хранится, пересобирается экспортом).
npm run i18n:sheet:export— собирает CSV: колонкаkey, дальше по колонке на локаль,enпервой. Новые ключи (заполнен толькоen) поднимаются наверх файла — переводчику сразу видно TODO.- Переводчик правит в Excel / Numbers / Google Sheets и сохраняет обратно в CSV (
.numbers/.xlsxне читаются). Разделитель определяется сам —,,;или таб, редакторы пишут по-разному. npm run i18n:sheet:import— раскладывает значения назад. Каждый файл пересобирается в своём порядке ключей (минимальный diff), непереведённый ключ остаётся отсутствующим, а не подменяется английским, чтобы отчёт продолжал его показывать.npm run i18n:check— отчёт, не гейт: рассинхрон с эталономen-US, мёртвые оверрайды, а в приложении ещё и SEO keyword-ссылки.
Удаление ключей едет от en-US: импорт выкидывает лист, которого больше нет в эталоне, так что устаревший
CSV не воскресит удалённый ключ.
Важное следствие двух наборов: непереведённый общий ключ чинится в CSV слоя — в CSV приложения он не появится вовсе. Отчёт приложения такие ключи показывает отдельным блоком «не переведено в слое» и не считает их своим рассинхроном.
Что мимо CSV
*_footer_links в приложении — это карты «keyword → URL категории», а не строки: сам ключ локализован
(en: twinks, ru: твинки), наборы по локалям разные by design. Они исключены из CSV и из сверки ключей,
курирует их разработчик/SEO прямо в JSON. Проверяется другое: каждый keyword должен встречаться в парном
*_footer_text, иначе ссылка не отрендерится — это и гоняет i18n:check в приложении.
Правила плюрализации (i18n/plurals.ts) и numberFormats остаются в приложении, хотя сами строки
plural.* общие: правила задаются на уровне локали, а не ключа.
Импорты
Внутри слоя — относительные пути (../../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/**, общие i18n-ключи (i18n/locales/— навигация, сортировки, UI-обвязка) — всё site-agnostic. - Приложение: только per-site — значения config (флаги/реклама/локали) через
runtimeConfig.public(слой держит пустые контейнеры-заглушки), свои i18n-ключи (SEO/ниша) и оверрайды общих,public/и выбор темы (theme:вconfig/site.config.ts). Своего SCSS у приложения нет:assets/scss/— пустой слот-оверрайд.server/целиком в слое, включая locale-кластер.
Конвенции
Полные правила — в 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.- Стили — 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 не падает,
но реальные значения приходят только из приложения.
Version History
| Version | Released | License |
|---|---|---|
| 0.0.1051 | ||
| 0.0.1050 | ||
| 0.0.1049 | ||
| 0.0.1047 | ||
| 0.0.1046 | ||
| 0.0.1045 | ||
| 0.0.1044 | ||
| 0.0.1043 | ||
| 0.0.1042 | ||
| 0.0.1041 | ||
| 0.0.1040 | ||
| 0.0.1039 | ||
| 0.0.1037 | ||
| 0.0.1036 | ||
| 0.0.1034 | ||
| 0.0.1031 | ||
| 0.0.1029 | ||
| 0.0.1028 | ||
| 0.0.1027 | ||
| 0.0.1026 | ||
| 0.0.1025 | ||
| 0.0.1024 | ||
| 0.0.1023 | ||
| 0.0.1021 | ||
| 0.0.1020 | ||
| 0.0.1018 | ||
| 0.0.1017 | ||
| 0.0.1013 | ||
| 0.0.1012 | ||
| 0.0.1011 |
Showing the latest 30 of 737. See every release on npm