sozsoft-platform/ui/vite.config.ts

327 lines
13 KiB
TypeScript
Raw Permalink Normal View History

2026-02-24 20:44:16 +00:00
import { defineConfig, loadEnv } from 'vite'
2026-08-12 19:08:14 +00:00
import type { Plugin } from 'vite'
2026-02-24 20:44:16 +00:00
import react from '@vitejs/plugin-react'
import path from 'path'
import { VitePWA } from 'vite-plugin-pwa'
2026-10-16 21:13:31 +00:00
import tailwindcss from '@tailwindcss/vite'
import twSafelistGenerator from './twSafelistGenerator.ts'
2026-02-24 20:44:16 +00:00
/**
* Precache stratejisi
* ------------------------------------------------------------------
2026-08-12 19:08:14 +00:00
* İki kademe vardır:
*
2026-08-12 19:08:14 +00:00
* 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)
2026-10-05 12:58:59 +00:00
* 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.
2026-08-12 19:08:14 +00:00
*
* `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',
2026-08-12 19:08:14 +00:00
'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',
]
2026-10-05 12:58:59 +00:00
/** Build çıktısındaki bütün JS/CSS chunk'ları; boyut ve eski sürüm temizliği için listelenir. */
2026-08-12 19:08:14 +00:00
const WARMUP_FILE = /^assets\/(?:js|css)\/.+\.(?:js|css)$/
2026-10-05 12:58:59 +00:00
/** Rota bileşeni olabilecek modüller (`dynamicRouteLoader` ile aynı glob kökü). */
const ROUTE_MODULE = /\/src\/(views\/.+)\.tsx$/
2026-08-12 19:08:14 +00:00
/**
* 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).
2026-10-05 12:58:59 +00:00
*
* `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.
2026-08-12 19:08:14 +00:00
*/
function warmupManifest(): Plugin {
return {
name: 'sozsoft-warmup-manifest',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
2026-10-05 12:58:59 +00:00
const outputs = Object.values(bundle).filter((output) => WARMUP_FILE.test(output.fileName))
2026-08-12 19:08:14 +00:00
// 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.
2026-10-05 12:58:59 +00:00
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))
}
2026-08-12 19:08:14 +00:00
this.emitFile({
type: 'asset',
fileName: 'warmup.json',
2026-10-05 12:58:59 +00:00
source: JSON.stringify({ files, routes }),
2026-08-12 19:08:14 +00:00
})
},
}
}
2026-09-22 20:59:36 +00:00
/**
* 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
2026-10-17 18:51:10 +00:00
* statik varsayilan gecerli kalir. Uretim konteynerinde ayni kural `nginx/default.conf`
* icindedir; burasi yalnizca dev sunucusu ve yerel `vite preview` icindir.
2026-09-22 20:59:36 +00:00
*/
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 }) => {
2026-02-24 20:44:16 +00:00
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'
2026-02-24 20:44:16 +00:00
return {
plugins: [
2026-10-16 21:13:31 +00:00
twSafelistGenerator(),
tailwindcss(),
2026-02-24 20:44:16 +00:00
react(),
2026-08-12 19:08:14 +00:00
warmupManifest(),
2026-02-24 20:44:16 +00:00
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,
2026-08-12 19:08:14 +00:00
// Precache indirmesinin ilerlemesini kullanıcıya gösterebilmek için
// service worker elle yazıldı.
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
2026-02-24 20:44:16 +00:00
devOptions: {
enabled: enableDevSw,
type: 'module',
2026-08-12 19:08:14 +00:00
navigateFallback: 'index.html',
2026-02-24 20:44:16 +00:00
},
2026-08-12 19:08:14 +00:00
injectManifest: {
globDirectory: 'dist',
globPatterns: PRECACHE_GLOBS,
// Kabuk dosyaları büyük olabilir (entry CSS ~1.3 MB).
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
},
2026-02-24 20:44:16 +00:00
manifest: {
2026-02-28 21:37:34 +00:00
name: 'Sözsoft Platform',
short_name: 'Sözsoft Platform',
2026-02-24 20:44:16 +00:00
theme_color: '#FF99C8',
background_color: '#f0e7db',
2026-08-12 19:08:14 +00:00
// 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: '/',
2026-02-24 20:44:16 +00:00
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'],
2026-02-28 21:37:34 +00:00
description: 'Sözsoft Platform Application',
2026-02-24 20:44:16 +00:00
},
}),
],
server: {
open: true,
port: 3000,
2026-09-22 20:59:36 +00:00
proxy: seoProxy(env.VITE_API_URL),
2026-02-24 20:44:16 +00:00
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'],
2026-02-24 20:44:16 +00:00
},
},
assetsInclude: ['**/*.md'],
resolve: {
alias: {
2026-08-12 12:20:00 +00:00
'@': path.join(import.meta.dirname, 'src'),
2026-02-24 20:44:16 +00:00
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',
],
},
2026-02-24 20:44:16 +00:00
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ü).
2026-02-24 20:44:16 +00:00
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,
2026-10-16 21:13:31 +00:00
rolldownOptions: {
2026-02-24 20:44:16 +00:00
output: {
2026-08-12 12:20:00 +00:00
// 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]'
2026-02-24 20:44:16 +00:00
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]'
}
2026-02-24 20:44:16 +00:00
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.
//
2026-08-16 15:30:07 +00:00
// 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
},
2026-02-24 20:44:16 +00:00
},
},
},
2026-10-17 18:51:10 +00:00
// Yalnızca yerel önizleme; üretim konteyneri nginx ile servis eder (Dockerfile).
2026-02-24 20:44:16 +00:00
preview: {
open: false,
2026-09-22 20:59:36 +00:00
proxy: seoProxy(env.VITE_API_URL),
2026-02-24 20:44:16 +00:00
},
define: {
'process.env': {},
},
}
})