sozsoft-platform/ui/vite.config.ts
2026-08-12 22:08:14 +03:00

301 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineConfig, loadEnv } from 'vite'
import type { Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import fs from 'fs'
import path from 'path'
import { VitePWA } from 'vite-plugin-pwa'
/**
* Precache stratejisi
* ------------------------------------------------------------------
* İki kademe vardır:
*
* 1. KABUK (workbox precache manifest'i, aşağıdaki `PRECACHE_GLOBS`)
* index.html + entry JS/CSS + her açılışta yüklenen vendor chunk'ları.
* Kurulumda ZORUNLUDUR; biri inmezse service worker kurulumu başarısız olur
* (yarım sürümle aktive olmak beyaz ekran demek).
*
* 2. BİLEŞENLER (`warmup.json`, bu dosyadaki plugin üretir)
* Tüm lazy chunk'lar ve CSS'leri. Güncelleme penceresi açıkken aynı ilerleme
* çubuğunda arka planda indirilir; böylece kullanıcı deploy'dan sonra ilk kez
* bir ekrana girdiğinde chunk beklemez. Best-effort'tur: inemeyen dosya
* kurulumu düşürmez, süre bütçesi aşılırsa kalanlar runtime cache'e kalır.
*
* `dist/css` altındaki 48 DevExtreme teması (~33 MB) bilerek dışarıdadır;
* kullanıcı başına yalnızca bir tema gerekir, runtime cache yeterlidir.
*
* Service worker `src/sw.ts` içinde elle yazıldı (strategies: injectManifest);
* çünkü güncelleme penceresinde indirilen her dosyanın adı/boyutu ve toplam
* yüzdesi gösterilebilsin diye indirme progress yayınlıyor.
*/
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',
'assets/js/vendor-react-*.js',
'assets/js/vendor-router-*.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',
]
/** Arka planda ısıtılacak bileşenler: uygulamanın tüm JS/CSS chunk'ları. */
const WARMUP_FILE = /^assets\/(?:js|css)\/.+\.(?:js|css)$/
/**
* Build çıktısındaki chunk adlarını ve (sıkıştırılmamış) boyutlarını
* `dist/warmup.json` olarak yazar. Boyutun derleme anında bilinmesi, service
* worker'ın ilerleme yüzdesini ilk saniyeden itibaren doğru göstermesini sağlar
* (Content-Length gzip'li boyut olduğu için yüzde zıplıyordu).
*/
function warmupManifest(): Plugin {
return {
name: 'sozsoft-warmup-manifest',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
const files = Object.values(bundle)
.filter((output) => WARMUP_FILE.test(output.fileName))
.map((output) => {
const source = output.type === 'chunk' ? output.code : output.source
return {
url: `/${output.fileName}`,
size:
typeof source === 'string'
? new TextEncoder().encode(source).byteLength
: source.byteLength,
}
})
// vite-plugin-pwa service worker'ı ayrı bir build ile üretir; o build'de
// eşleşen dosya olmaz. Boş liste yazmak gerçek manifest'i ezerdi.
if (files.length === 0) return
this.emitFile({
type: 'asset',
fileName: 'warmup.json',
source: JSON.stringify({ files }),
})
},
}
}
/**
* `scripts/generate-version.js` build'den önce çalışır. Buradaki değerleri
* bundle'a gömüyoruz ki uygulama, çalışan kodun build kimliğini sunucudaki
* `version.json` ile birebir karşılaştırıp yeni deploy'u kesin olarak anlasın.
*/
function readBuildInfo() {
try {
const file = path.join(import.meta.dirname, 'public', 'version.json')
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
return {
buildId: String(data.buildId ?? data.commit ?? 'dev'),
version: String(data.version ?? data.releases?.[0]?.version ?? '0.0.0'),
}
} catch {
return { buildId: 'dev', version: '0.0.0' }
}
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const buildInfo = readBuildInfo()
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(),
warmupManifest(),
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,
// Precache indirmesinin ilerlemesini kullanıcıya gösterebilmek için
// service worker elle yazıldı.
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
devOptions: {
enabled: enableDevSw,
type: 'module',
navigateFallback: 'index.html',
},
injectManifest: {
globDirectory: 'dist',
globPatterns: PRECACHE_GLOBS,
// Kabuk dosyaları büyük olabilir (entry CSS ~1.3 MB).
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
},
manifest: {
name: 'Sözsoft Platform',
short_name: 'Sözsoft Platform',
theme_color: '#FF99C8',
background_color: '#f0e7db',
// Tarayıcının "uygulamayı yükle" akışı (beforeinstallprompt) yalnızca
// standalone/minimal-ui görüntü modlarında tetiklenir.
display: 'standalone',
start_url: '/',
scope: '/',
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(import.meta.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',
],
},
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: {
// Eski `esbuild.legalComments: 'none'` karşılığı. `annotation`
// (@__PURE__) ve `jsdoc` varsayılan olarak korunur; annotation
// yorumları tree-shaking için gereklidir.
// `debugger` ifadeleri oxc minifier tarafından zaten düşürülüyor
// (compress.dropDebugger varsayılanı true), ayrıca ayar gerekmiyor.
comments: { legal: false },
// `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': {},
// Çalışan bundle'ın kimliği. İstemci bunu sunucudaki version.json ile
// karşılaştırarak yeni deploy'u kesin olarak (tag'e ihtiyaç duymadan) anlar.
__APP_BUILD_ID__: JSON.stringify(buildInfo.buildId),
__APP_VERSION__: JSON.stringify(buildInfo.version),
},
}
})