Newer
Older
gnexus-tasks / frontend / vite.config.ts
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 + precache статики приложения.
    // Оффлайн-режим данных (кэш /api) — открытый вопрос ТЗ 8.4, пока всегда онлайн.
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'GNexus Tasks',
        short_name: 'Tasks',
        description: 'Персональный менеджер задач',
        lang: 'ru',
        start_url: '/',
        display: 'standalone',
        background_color: '#ffffff',
        theme_color: '#1f9d6c',
        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',
          },
        ],
      },
      workbox: {
        // CSS кита/phosphor ~4 МБ — поднимаем лимит precache, иначе оффлайн-оболочка неполна
        maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
        // /api и /auth никогда не кэшируются и не получают fallback на index.html
        navigateFallbackDenylist: [/^\/api\//, /^\/auth\//, /^\/mcp\//],
        runtimeCaching: [
          {
            // вложения — только из кэша, если попали туда ранее; сеть не дублируем
            urlPattern: ({ url, request }) =>
              (url.pathname.startsWith('/api/attachments/') ||
                url.pathname.startsWith('/assets/')) &&
              request.method === 'GET',
            handler: 'CacheFirst',
            options: {
              cacheName: 'attachments-and-assets',
              expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 },
              cacheableResponse: { statuses: [0, 200] },
            },
          },
        ],
      },
    }),
  ],
  server: {
    port: 15134, // 5173 занят другими проектами; мнемоника: 1 + API 8134
    proxy: {
      '/api': {
        target: 'http://localhost:8134',
        changeOrigin: true,
      },
      '/auth': {
        target: 'http://localhost:8134',
        changeOrigin: true,
      },
    },
  },
})