import { createReadStream, existsSync, statSync, cpSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import vue from '@vitejs/plugin-vue'
// kit.css ссылается на шрифты/иконки через абсолютные /assets/... URL —
// по контракту кита хост обязан отдавать его dist/assets на /assets.
// В dev — middleware, в build — копирование в dist/assets.
const kitAssetsDir = path.resolve(
fileURLToPath(new URL('.', import.meta.url)),
'node_modules/gnexus-ui-kit/dist/assets',
)
const mimeByExt: Record<string, string> = {
'.css': 'text/css',
'.js': 'text/javascript',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
}
function kitAssetsPlugin(): Plugin {
return {
name: 'gnexus-kit-assets',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const url = (req.url ?? '').split('?')[0]
if (!url.startsWith('/assets/')) return next()
const file = path.join(kitAssetsDir, url.slice('/assets/'.length))
if (!existsSync(file) || !statSync(file).isFile()) return next()
res.setHeader('Content-Type', mimeByExt[path.extname(file)] ?? 'application/octet-stream')
createReadStream(file).pipe(res)
})
},
closeBundle() {
cpSync(kitAssetsDir, path.resolve(fileURLToPath(new URL('.', import.meta.url)), 'dist/assets'), {
recursive: true,
})
},
}
}
// Dev: /api и /auth проксируются на backend. В проде nginx отдаёт статику и проксирует сам.
export default defineConfig({
plugins: [
vue(),
kitAssetsPlugin(),
// PWA (M6): manifest + свой service worker (src/sw.ts → dist/sw.js).
// Оффлайн-режим данных (кэш /api) — открытый вопрос ТЗ 8.4, пока всегда онлайн.
// strategies: injectManifest нужен ради обработчика push (ТЗ 3.21): в
// generateSW его некуда вставить, а precache/fallback/CacheFirst из блока
// workbox в этом режиме не применяются — они переехали в сам src/sw.ts
VitePWA({
registerType: 'autoUpdate',
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
injectManifest: {
// CSS кита/phosphor ~4 МБ — поднимаем лимит precache, иначе оффлайн-оболочка неполна
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
},
manifest: {
name: 'GNexus Tasks',
short_name: 'Tasks',
description: 'Персональный менеджер задач',
lang: 'ru',
id: '/',
start_url: '/',
display: 'standalone',
// тёмная палитра кита: иначе при установке на телефон белый экран
// мигает до загрузки CSS, а шапка окна зеленеет
background_color: '#16161e',
theme_color: '#16161e',
icons: [
{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
{
src: '/icons/maskable-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
// В dev воркер не регистрируется (ТЗ 3.15): он кэшировал бы статику поверх
// HMR. Флаг VITE_PWA_DEV=1 поднимает его для отладки push — там нужен
// именно модульный воркер, иначе injectManifest не соберётся
devOptions: { enabled: !!process.env.VITE_PWA_DEV, type: 'module' },
}),
],
server: {
port: 15134, // 5173 занят другими проектами; мнемоника: 1 + API 8134
proxy: {
'/api': {
target: 'http://localhost:8134',
changeOrigin: true,
},
'/auth': {
target: 'http://localhost:8134',
changeOrigin: true,
},
// MCP-сервер того же backend — адрес страницы-инструкции /mcp-help
// совпадает с адресом приложения (http-proxy стримит SSE нормально).
// Регэксп, а не префикс: путь страницы /mcp-help не должен проксироваться
'^/mcp(/|$)': {
target: 'http://localhost:8134',
changeOrigin: true,
},
},
},
})