import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' import { VitePWA } from 'vite-plugin-pwa' /** * Precache stratejisi * ------------------------------------------------------------------ * `dist` klasörü ~55 MB (48 adet DevExtreme teması ~33 MB + lazy chunk'lar * ~20 MB). Bunların tamamını precache etmek her deploy'da service worker'ın * on-larca MB indirmesine, mobil/yavaş bağlantıda kurulumun timeout'a düşüp * worker'ın `redundant` olmasına ve güncellemenin hiç tamamlanmamasına yol * açıyordu. * * Bu yüzden precache SADECE uygulama kabuğudur (index.html + entry JS + CSS, * ~2.8 MB). Geri kalan her şey (lazy chunk, tema CSS'i, görsel, font) runtime * cache ile ilk kullanımda saklanır. Dosya adları hash'li olduğu için * CacheFirst güvenlidir; hash'siz public dosyalarda StaleWhileRevalidate * kullanılır. */ const PRECACHE_GLOBS = [ 'index.html', // manifest.webmanifest ve manifest ikonları vite-plugin-pwa tarafından // otomatik eklenir; buraya yazmak workbox'ta "conflicting entries" hatası verir. 'favicon.ico', 'assets/js/entry-*.js', // Sadece entry CSS'i. Rapor tasarımcısı/görüntüleyici CSS'leri (~580 KB) // lazy yüklendiği için runtime cache'e bırakılır. 'assets/css/index-*.css', ] // Deploy'da silinen eski build dosyalarını precache manifestine sokmamak için // runtime cache'e alınacak hash'li uygulama varlıkları. const HASHED_ASSET_PATTERN = /\/assets\/.+\.(?:js|css|wasm)$/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') const isProduction = mode === 'production' const usePolling = env.VITE_USE_POLLING === 'true' // Dev'de service worker varsayılan olarak kapalıdır; açık olduğunda HMR // sonrası eski chunk'lar cache'den servis edilip kafa karıştırıyor. const enableDevSw = env.VITE_PWA_DEV === 'true' const baseDomains = ['sozsoft.com', 'dev.sozsoft.com', '.sozsoft.com'] const allowedHosts = Array.from(new Set(['localhost', ...baseDomains])) return { plugins: [ react(), VitePWA({ // Yeni sürüm önce tamamen indirilir, ardından uygulama kontrollü olarak // yeni service worker'a geçer. Böylece eski ekran ile yeni chunk'lar karışmaz. registerType: 'prompt', // Kayıt ve güncelleme akışı src/views/version/swRegistration.ts tarafından yönetilir. injectRegister: false, devOptions: { enabled: enableDevSw, type: 'module', }, workbox: { globDirectory: 'dist', globPatterns: PRECACHE_GLOBS, // Kabuk dosyaları büyük olabilir (entry CSS ~1.3 MB). maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // Yeni worker precache'i bitirdikten sonra uygulama SKIP_WAITING // gönderir; clientsClaim açık sekmelerin kontrolünü o an devralır. clientsClaim: true, skipWaiting: false, cleanupOutdatedCaches: true, navigateFallback: '/index.html', navigateFallbackAllowlist: [/^(?!\/__).*/], navigateFallbackDenylist: [/^\/api\//, /^\/connect\//, /^\/swagger/, /\/version\.json$/], runtimeCaching: [ { // Sürüm/changelog bilgisi asla eski kalmamalı. urlPattern: /\/version\.json$/, handler: 'NetworkFirst', options: { cacheName: 'app-version-v2', networkTimeoutSeconds: 3, expiration: { maxEntries: 2, maxAgeSeconds: 24 * 60 * 60 }, cacheableResponse: { statuses: [0, 200] }, }, }, { // Hash'li build çıktıları immutable'dır. urlPattern: HASHED_ASSET_PATTERN, handler: 'CacheFirst', options: { cacheName: 'app-assets-v3', expiration: { maxEntries: 400, maxAgeSeconds: 60 * 24 * 60 * 60 }, cacheableResponse: { statuses: [0, 200] }, }, }, { // public/css altındaki DevExtreme temaları hash'siz; içerik // sürümle değişebildiği için arka planda tazelenir. urlPattern: /\/css\/[\w.-]+\.css$/, handler: 'StaleWhileRevalidate', options: { cacheName: 'dx-themes-v1', expiration: { maxEntries: 12, maxAgeSeconds: 30 * 24 * 60 * 60 }, cacheableResponse: { statuses: [0, 200] }, }, }, { urlPattern: /\.(?:woff2?|ttf|otf|eot)$/, handler: 'CacheFirst', options: { cacheName: 'fonts-v1', expiration: { maxEntries: 40, maxAgeSeconds: 180 * 24 * 60 * 60 }, cacheableResponse: { statuses: [0, 200] }, }, }, { urlPattern: /\.(?:png|jpe?g|svg|gif|webp|ico)$/, handler: 'CacheFirst', options: { cacheName: 'images-v2', expiration: { maxEntries: 150, maxAgeSeconds: 30 * 24 * 60 * 60 }, cacheableResponse: { statuses: [0, 200] }, }, }, ], }, manifest: { name: 'Sözsoft Platform', short_name: 'Sözsoft Platform', theme_color: '#FF99C8', background_color: '#f0e7db', display: 'browser', icons: [ { src: '/img/logo/logo-400.png', sizes: '400x400', type: 'image/png', purpose: 'any maskable', }, { src: '/img/logo/logo-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable', }, { src: '/img/logo/logo-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable', }, ], categories: ['business', 'productivity'], description: 'Sözsoft Platform Application', }, }), ], server: { open: true, port: 3000, watch: { usePolling, interval: usePolling ? 1000 : undefined, }, // Sık dokunulan giriş dosyalarını önceden transform ederek ilk // gezinmedeki dev sunucu gecikmesini azalt. warmup: { clientFiles: ['./src/main.tsx', './src/App.tsx', './src/components/layouts/index.ts'], }, }, assetsInclude: ['**/*.md'], resolve: { alias: { '@': path.join(__dirname, 'src'), inferno: 'inferno/dist/index.esm.js', 'devextreme/ui': 'devextreme/esm/ui', }, }, optimizeDeps: { // Barrel/CJS ağırlıklı paketleri dev cold-start'ta tek seferde // prebundle et; aksi halde ilk sayfa yüzlerce ayrı istek atıyor. include: [ 'react', 'react-dom', 'react-dom/client', 'react-router-dom', '@tanstack/react-query', '@tanstack/react-table', 'axios', 'dayjs', 'formik', 'yup', 'classnames', 'lodash', 'easy-peasy', 'framer-motion', 'react-icons/fa', // CJS/UMD bundle. `exclude` edilirse interop bozulup // "Babel.transform is not a function" hatası veriyor. Ayrıca burada // listelenince dinamik import anındaki "new dependency optimized" // kaynaklı sayfa yenilemesi de olmuyor. '@babel/standalone', ], }, esbuild: { legalComments: 'none', drop: isProduction ? ['debugger'] : [], }, build: { outDir: 'dist', // Vite'ın varsayılan 'baseline-widely-available' hedefi bilerek korunuyor: // es2022'ye sabitlemek esbuild'i downlevel'a zorlayıp entry chunk'ı // ~200 KB büyütüyor (ölçüldü). chunkSizeWarningLimit: 2000, sourcemap: false, emptyOutDir: true, cssCodeSplit: true, // Modern tarayıcı hedefinde polyfill gereksiz; entry'den birkaç KB düşer. modulePreload: { polyfill: false }, // Build sonunda gzip boyutu hesaplamak bu boyutta bir çıktıda dakikalar sürüyor. reportCompressedSize: false, rollupOptions: { output: { // `entry-` öneki precache glob'unun sadece gerçek entry chunk'ı // yakalamasını sağlar (node_modules kaynaklı `index-*.js` chunk'ları // yanlışlıkla precache'e girmesin). entryFileNames: 'assets/js/entry-[name]-[hash].js', chunkFileNames: 'assets/js/[name]-[hash].js', assetFileNames: ({ name }: { name?: string }) => { if (/\.css$/.test(name ?? '')) return 'assets/css/[name]-[hash][extname]' if (/\.(png|jpe?g|svg|gif|ico|webp)$/.test(name ?? '')) { return 'assets/img/[name]-[hash][extname]' } if (/\.(woff2?|ttf|otf|eot)$/.test(name ?? '')) { return 'assets/fonts/[name]-[hash][extname]' } return 'assets/[name]-[hash][extname]' }, // SADECE her açılışta zaten yüklenen (%100 eager) çekirdek vendor'lar // ayrılır; bunlar sürümler arası değişmediği için tarayıcı cache'inde // kalır ve deploy sonrası yeniden indirilmez. // // Not: framer-motion / lodash / formik gibi paketleri de buraya eklemek // ölçüldü ve zararlı çıktı — Rollup'ın otomatik chunk'lamasında bu // paketlerin bir kısmı yalnızca lazy view'lara giriyor; tek chunk'a // toplandıklarında ilk yükleme ~650 KB büyüyor. manualChunks(id) { if (!id.includes('node_modules')) return undefined const file = id.replace(/\\/g, '/') if (/\/node_modules\/(react|react-dom|scheduler)\//.test(file)) return 'vendor-react' if (/\/node_modules\/(react-router|react-router-dom|@remix-run)\//.test(file)) { return 'vendor-router' } return undefined }, }, }, }, preview: { host: '0.0.0.0', port: 80, open: false, allowedHosts, }, define: { 'process.env': {}, }, } })