# 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 // __FEATURES__ типизируется сгенерированным .vite-layers/features.d.ts — declare не нужен const routes = [ { path: '/', component: () => import('@/pages/Home') }, ...(__FEATURES__.billing ? [{ path: '/billing', component: () => import('@/pages/Billing') }] : []), ] ``` Тип `__FEATURES__` генерируется из `merged.features` в `.vite-layers/features.d.ts`, поэтому опечатка (`__FEATURES__.biling`) — ошибка компиляции, а не молчком-falsy. Флаги вшиваются через `define` и сворачиваются esbuild ещё **до** построения графа Rollup, поэтому выключенная ветка и её `import()` физически не попадают в бандл. Дотированные литералы эмитятся на любую глубину (`__FEATURES__.nested.enabled` тоже сворачивается). Правила, чтобы DCE сработало: - обращайтесь **напрямую** — `__FEATURES__.billing`; алиас/деструктуризация (`const f = __FEATURES__; f.billing`) и динамический доступ (`__FEATURES__[name]`) не сворачиваются; - гейт оборачивает сам `import()` (тернарник/`&&`/спред), а не `.filter` после — reachable-импорт не вырезается; - ключи фич — валидные JS-идентификаторы (kebab/пробел доступны в рантайме через объект `__FEATURES__`, но без DCE); - в тестах продублируйте `define` (в `vitest.config`) или гардите `globalThis.__FEATURES__ ?? {}`. **Dev-режим.** В build флаги работают через `define` (+ DCE). В dev Vite 8 / rolldown-vite **не** инлайнит пользовательский `define` в исходники, поэтому `vite-layers` сам подставляет `__FEATURES__` в рантайме (dev-only плагин). Плюс при изменении любого `app.config.*` слоя dev-сервер **автоматически перезапускается** (`app.config` грузится c12, вне графа Vite — сам он не следит) — так фичи обновляются без ручного рестарта. В шаблонах `.vue` `__FEATURES__` напрямую использовать нельзя — компилятор префиксует его в `_ctx.__FEATURES__` (define/рантайм-подстановка не матчат); читайте флаг в `