# vite-layers Framework-agnostic **слои в стиле Nuxt**, портированные на чистый Vite: файловые оверрайды через `extends` + мёрж конфигов, плюс побрендовый build-time dead-code elimination. Работает с любым фреймворком — у ядра **нет фреймворк-зависимостей**; `vue()`/`react()`/и т.п. подключаются на уровне слоя. Логика стека слоёв (порядок, дедуп, авто-скан, алиасы) портирована напрямую из исходников Nuxt (`@nuxt/kit` `loadNuxtConfig` + `@nuxt/schema`), с тремя осознанными улучшениями. ## Установка ```bash pnpm add -D vite-layers # peer-зависимость: vite ^8 ``` ## Использование Каждое приложение/бренд — это директория с `app.config.ts` и однострочным `vite.config.ts`: ```ts // apps/main/app.config.ts import { defineLayerConfig } from 'vite-layers' export default defineLayerConfig({ name: 'main', features: { billing: true }, vite: { plugins: [vue()] }, // фреймворк-плагин живёт здесь, а не в ядре }) // apps/brand/app.config.ts — только диффы import { defineLayerConfig } from 'vite-layers' export default defineLayerConfig({ name: 'brand', extends: ['../main'], features: { billing: false }, // бренд полностью убирает страницу billing }) // apps/<любой>/vite.config.ts import { buildViteConfig } from 'vite-layers' export default buildViteConfig(import.meta.dirname) ``` Чтобы перекрыть файл, положите его по тому же относительному пути в слое с более высоким приоритетом (`apps/brand/src/components/Header.vue` затеняет `apps/main/src/components/Header.vue`). Гейтите опциональные страницы так, чтобы выключенные **исчезали из бандла** (а не просто переставали роутиться): ```ts import { feature } from '#feature' // алиас регистрирует vite-layers; типы — из .vite-layers/features.d.ts const routes = [ { path: '/', component: () => import('@/pages/Home') }, ...(feature('billing') ? [{ path: '/billing', component: () => import('@/pages/Billing') }] : []), ] ``` `feature('key')` — **компайл-тайм макрос**: плагин заменяет вызов на литерал значения флага, **одинаково в dev и в build** (один AST-transform, без расхождений). Подставленный `false` делает ветку статически мёртвой, и Rollup/rolldown вырезает её вместе с `import()` — чанк выключенной фичи не эмитится. Тип `feature` генерируется из `merged.features` **литеральными типами**, поэтому опечатка ключа (`feature('biling')`) — ошибка компиляции. Правила (они **enforced**: нарушение валит сборку — и в dev, и в build, ничего не «протекает» молча): - ключ — строковый литерал: `feature('billing')`, не `feature(name)`; - вызывайте напрямую — без алиасов (`const f = feature`), деструктуризации и передачи как значения; - вложенные флаги — дотированным ключом: `feature('payments.stripe')`; - ключ должен существовать в `merged.features` (иначе — ошибка сборки). Тестам ничего дублировать не нужно — тот же transform работает в Vitest (плагин — часть конфига). В `.vue`-шаблоне напрямую `feature('x')` использовать нельзя (компилятор делает из него `_ctx.feature` — это не вызов макроса): читайте флаг в `