sozsoft-platform/ui/vite.config.ts
2026-10-17 21:51:10 +03:00

326 lines
13 KiB
TypeScript
Raw Permalink 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 path from 'path'
import { VitePWA } from 'vite-plugin-pwa'
import tailwindcss from '@tailwindcss/vite'
import twSafelistGenerator from './twSafelistGenerator.ts'
/**
* 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)
* Kurulumun parçası değildir. Giriş yapmış kullanıcının yetkili olduğu
* rotaların chunk'ları ve CSS'leri uygulama açıldıktan sonra arka planda
* indirilir; ziyaretçi ve giriş ekranı hiçbir bileşen indirmez. Best-effort'tur:
* inemeyen dosya atlanır, 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',
]
/** Build çıktısındaki bütün JS/CSS chunk'ları; boyut ve eski sürüm temizliği için listelenir. */
const WARMUP_FILE = /^assets\/(?:js|css)\/.+\.(?:js|css)$/
/** Rota bileşeni olabilecek modüller (`dynamicRouteLoader` ile aynı glob kökü). */
const ROUTE_MODULE = /\/src\/(views\/.+)\.tsx$/
/**
* 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).
*
* `routes` her view modülünün açılması için gereken dosyaları (`files` içindeki
* sıra numaraları) taşır: chunk'ın kendisi, statik import zinciri ve CSS'leri.
* Service worker yalnızca kullanıcının yetkili olduğu rotaların zincirini ısıtır;
* ekranın içinden lazy açılan parçalar (diyalog, editör) runtime cache'e kalır.
*/
function warmupManifest(): Plugin {
return {
name: 'sozsoft-warmup-manifest',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
const outputs = Object.values(bundle).filter((output) => WARMUP_FILE.test(output.fileName))
// 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 (outputs.length === 0) return
const indexOf = new Map(outputs.map((output, index) => [output.fileName, index]))
const files = outputs.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,
}
})
const closure = (fileName: string, seen: Set<number>) => {
const index = indexOf.get(fileName)
if (index === undefined || seen.has(index)) return
seen.add(index)
const output = bundle[fileName]
if (output?.type !== 'chunk') return
output.viteMetadata?.importedCss.forEach((css) => closure(css, seen))
output.imports.forEach((imported) => closure(imported, seen))
}
// Başka bir modülün statik olarak da import ettiği view kendi chunk'ını almaz,
// o chunk'ın içine katılır; bu yüzden eşleme chunk'ın facade'ından değil
// içerdiği modüllerden kurulur.
const routes: Record<string, number[]> = {}
for (const output of outputs) {
if (output.type !== 'chunk') continue
const views = output.moduleIds
.map((id) => id.replace(/\\/g, '/').match(ROUTE_MODULE)?.[1])
.filter((view): view is string => !!view)
if (views.length === 0) continue
const seen = new Set<number>()
closure(output.fileName, seen)
const files = [...seen]
views.forEach((view) => (routes[view] = files))
}
this.emitFile({
type: 'asset',
fileName: 'warmup.json',
source: JSON.stringify({ files, routes }),
})
},
}
}
/**
* Arama motorlari `sitemap.xml` ve `robots.txt` dosyalarini **sitenin** kok dizininde arar;
* ikisini de API uretiyor (SeoController). Bu iki yolu UI sunucusundan API'ye gecirmek,
* ayri bir ters vekil kurali gerektirmeden dosyalarin dogru alan adindan yayinlanmasini
* saglar. `VITE_API_URL` tanimli degilse proxy kurulmaz ve `public/robots.txt` icindeki
* statik varsayilan gecerli kalir. Uretim konteynerinde ayni kural `nginx/default.conf`
* icindedir; burasi yalnizca dev sunucusu ve yerel `vite preview` icindir.
*/
function seoProxy(apiUrl: string | undefined) {
if (!apiUrl) return undefined
const target = { target: apiUrl, changeOrigin: true, secure: false }
return { '/sitemap.xml': target, '/robots.txt': target }
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
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'
return {
plugins: [
twSafelistGenerator(),
tailwindcss(),
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,
proxy: seoProxy(env.VITE_API_URL),
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',
'axios',
'dayjs',
'formik',
'yup',
'classnames',
'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,
rolldownOptions: {
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 / 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
},
},
},
},
// Yalnızca yerel önizleme; üretim konteyneri nginx ile servis eder (Dockerfile).
preview: {
open: false,
proxy: seoProxy(env.VITE_API_URL),
},
define: {
'process.env': {},
},
}
})