Claude vite optimize ve check new version

This commit is contained in:
Sedat ÖZTÜRK 2026-08-07 14:59:47 +03:00
parent 81960df31b
commit 93e337b9b2
9 changed files with 837 additions and 395 deletions

View file

@ -20586,6 +20586,30 @@
"en": "Update check failed.",
"tr": "Güncelleme kontrolü başarısız"
},
{
"resourceName": "Platform",
"key": "App.UpdateCheckDisabled",
"en": "Update check is disabled",
"tr": "Güncelleme kontrolü kapalı"
},
{
"resourceName": "Platform",
"key": "App.UpdateCheckDisabledInThisEnvironment",
"en": "The service worker is disabled in this environment, so the version cannot be checked.",
"tr": "Bu ortamda service worker devre dışı olduğu için sürüm kontrolü yapılamıyor."
},
{
"resourceName": "Platform",
"key": "App.UpdateCompleted",
"en": "The application has been updated",
"tr": "Uygulama güncellendi"
},
{
"resourceName": "Platform",
"key": "App.UpdatedToVersion",
"en": "New version",
"tr": "Yeni sürüm"
},
{
"resourceName": "Platform",
"key": "App.StaticLookup.AbpAccount",

View file

@ -1,6 +1,28 @@
{
"commit": "8146adf",
"commit": "81960df",
"releases": [
{
"version": "1.1.10",
"buildDate": "2026-07-31",
"commit": "2eef3ead35848341350659828a14dc4a463e57af",
"changeLog": [
"- Kullanıcılara User Type sütunu eklendi. Hem \"İç Kullanıcılar\" hemde \"Dış Kullanıcılar\" tanımlanabiliyor.",
"- Lisans adedi sadece İç Kullanıcılar üzerinden çalışıyor.",
"- Scheduler ve TodoBard komponentler geliştirildi.",
"- Lookup editorlerin Static değerlere dil desteği eklendi."
]
},
{
"version": "1.1.09",
"buildDate": "2026-07-29",
"commit": "b0e78ac619dfebac2eb17edfc04551217ca4b93e",
"changeLog": [
"- Wizard yetenekleri artırıldı. Menu Order eklendi.",
"- GanttView sample düzenlemeleri",
"- Schedulerview sample düzenlemeleri",
"- CryptographyXml version 10.0.10"
]
},
{
"version": "1.1.08",
"buildDate": "2026-07-16",

View file

@ -11,6 +11,7 @@ import { Helmet } from 'react-helmet'
import { useNavigate, useLocation } from 'react-router-dom'
import { getSetupStatus } from '@/services/setup.service'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useAppVersionNotice } from '@/views/version/useAppVersionNotice'
let didInit = false
@ -62,6 +63,7 @@ const Theme = (props: CommonProps) => {
useDarkMode()
useTabFocus()
useNotification()
useAppVersionNotice()
type ConfigProviderValue = NonNullable<ComponentProps<typeof ConfigProvider>['value']>

View file

@ -47,9 +47,6 @@ const validationSchema = Yup.object().shape({
const Login = () => {
const navigate = useNavigate()
const UiVersion = useStoreState((state) => state.locale.currentUiVersion)
const { setUiVersion } = useStoreActions((a) => a.locale)
const [message, setMessage] = useState('')
const [error, setError] = useTimeOutMessage(300000)
const [twoFactor, setTwoFactor] = useState(false)
@ -152,9 +149,6 @@ const Login = () => {
setShowCaptcha(false)
setError('')
setMessage('')
// Versiyon kontrolü
findUiVersion()
}
if (showCaptcha) {
@ -165,19 +159,6 @@ const Login = () => {
setSubmitting(false)
}
const findUiVersion = async () => {
try {
const res = await fetch(`/version.json?ts=${Date.now()}`)
const latest = (await res.json())?.releases?.[0]?.version
if (latest && UiVersion !== latest) {
setUiVersion(latest)
navigate(ROUTES_ENUM.protected.admin.changeLog)
}
} catch (e) {
console.warn('Versiyon okunamadı', e)
}
}
return (
<>
<Helmet

View file

@ -20,6 +20,13 @@ export const useManualUpdateCheck = () => {
const { translate } = useLocalization()
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
// translate() eksik anahtarda anahtarın kendisini döndürüyor; kullanıcıya
// "::App.X" göstermemek için okunabilir bir karşılığa düş.
const label = (key: string, fallback: string) => {
const value = translate(key)
return !value || value === key || value.includes('::') ? fallback : value
}
const checkForUpdate = async () => {
if (isCheckingUpdate) return
setIsCheckingUpdate(true)
@ -34,6 +41,19 @@ export const useManualUpdateCheck = () => {
</Notification>,
{ placement: 'bottom-end' },
)
} else if (result === 'disabled') {
toast.push(
<Notification
title={label('::App.UpdateCheckDisabled', 'Güncelleme kontrolü kapalı')}
type="info"
>
{label(
'::App.UpdateCheckDisabledInThisEnvironment',
'Bu ortamda service worker devre dışı olduğu için sürüm kontrolü yapılamıyor.',
)}
</Notification>,
{ placement: 'bottom-end' },
)
} else if (result === 'unsupported') {
toast.push(
<Notification title={translate('::App.UpdateCouldNotBeChecked')} type="warning">

View file

@ -1,122 +1,132 @@
import { registerSW } from 'virtual:pwa-register'
import { store } from '@/store'
const ACTIVATION_RETRY_DELAY = 5_000
const ACTIVATION_TIMEOUT = 20_000
/**
* Service worker güncelleme akışı
* ------------------------------------------------------------------
* 1. Kayıt + ılışta bir kez `update()` kontrolü.
* 2. Sekme görünürken periyodik, sekmeye dönüldüğünde ve internet geri
* geldiğinde tekrar kontrol -> yeni deploy ık uygulamada da yakalanır.
* 3. Yeni worker kurulmaya başlayınca (`installing`) engellemeyen bir bant
* ile "yeni sürüm indiriliyor" bilgisi gösterilir.
* 4. Kurulum bitince (`installed`) tam ekran overlay + sürüm notları gösterilir,
* SKIP_WAITING gönderilir ve `controllerchange` ile sayfa yenilenir.
* 5. Yenileme sonrası `consumeAppUpdatedVersion()` ile "güncellendi" bildirimi
* ve changelog yönlendirmesi yapılır.
*/
let registrationStarted = false
let activationInProgress = false
let reloadStarted = false
let currentRegistration: ServiceWorkerRegistration | undefined
let activateUpdateHandler: ((reloadPage?: boolean) => Promise<void>) | undefined
let registrationInitialization: Promise<void> | undefined
let activationRetryTimer: number | undefined
let activationTimeoutTimer: number | undefined
let overlayStoreUnsubscribe: (() => void) | undefined
const watchedInstallingWorkers = new WeakSet<ServiceWorker>()
export type ManualUpdateResult =
| 'up-to-date'
| 'updating'
/** Tarayıcı service worker desteklemiyor. */
| 'unsupported'
/** Destekleniyor ama bu ortamda kapalı (dev; VITE_PWA_DEV=true ile açılır). */
| 'disabled'
export type ManualUpdateResult = 'up-to-date' | 'updating' | 'unsupported'
const PERIODIC_CHECK_INTERVAL = 15 * 60 * 1000
const MIN_CHECK_GAP = 60 * 1000
const ACTIVATION_RETRY_DELAY = 8_000
const ACTIVATION_TIMEOUT = 30_000
// `virtual:pwa-register` dev'de (devOptions kapalıyken) no-op'tur ve hiçbir
// callback çalışmaz. Kayıt promise'inin süresiz beklememesi için üst sınır.
const REGISTRATION_TIMEOUT = 10_000
// `registration.update()` yavaş/kopuk ağda süresiz askıda kalabiliyor.
const UPDATE_CALL_TIMEOUT = 20_000
const withTimeout = <T>(promise: Promise<T>, ms: number) =>
Promise.race([
promise,
new Promise<undefined>((resolve) => window.setTimeout(() => resolve(undefined), ms)),
])
const UPDATED_VERSION_KEY = 'sozsoft:app-updated-version'
const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload'
type Release = {
version: string
changeLog?: string[]
}
const getText = (value: unknown) =>
value === undefined || value === null ? undefined : String(value)
let started = false
let registration: ServiceWorkerRegistration | undefined
let initialization: Promise<void> | undefined
let activating = false
let reloading = false
let lastCheckAt = 0
let periodicTimer: number | undefined
let activationRetryTimer: number | undefined
let activationTimeoutTimer: number | undefined
let storeUnsubscribe: (() => void) | undefined
let latestVersion: string | undefined
let releasePromise: Promise<Release | undefined> | undefined
function updateOverlayApiVersion(overlay: HTMLElement) {
const apiConfig = store.getState().abpConfig.config?.extraProperties
const apiEnvironment = getText(apiConfig?.environment)
const apiVersion = getText(apiConfig?.version)
const watchedRegistrations = new WeakSet<ServiceWorkerRegistration>()
const watchedWorkers = new WeakSet<ServiceWorker>()
if (!apiEnvironment && !apiVersion) return false
/* ------------------------------------------------------------------ */
/* Sürüm bilgisi */
/* ------------------------------------------------------------------ */
const apiValue = overlay.querySelector<HTMLElement>('[data-sw-api-version]')
if (apiValue) {
apiValue.textContent = `${apiEnvironment ?? '-'}:${apiVersion ?? '-'}`
}
return true
}
function watchOverlayApiVersion(overlay: HTMLElement) {
overlayStoreUnsubscribe?.()
if (updateOverlayApiVersion(overlay)) return
overlayStoreUnsubscribe = store.subscribe(() => {
if (!overlay.isConnected || updateOverlayApiVersion(overlay)) {
overlayStoreUnsubscribe?.()
overlayStoreUnsubscribe = undefined
}
})
}
async function loadUpdateOverlayDetails(overlay: HTMLElement) {
const state = store.getState()
const uiEnvironment = import.meta.env.MODE
let uiVersion = state.locale.currentUiVersion
let latestRelease: Release | undefined
const loadLatestRelease = () =>
(releasePromise ??= fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store' })
.then((response) => {
if (!response.ok) throw new Error(`version.json isteği ${response.status} döndü`)
return response.json() as Promise<{ releases?: Release[] }>
})
.then((data) => {
const release = data.releases?.[0]
latestVersion = release?.version ?? latestVersion
return release
})
.catch((error: unknown) => {
console.warn('Sürüm bilgisi okunamadı.', error)
releasePromise = undefined
return undefined
}))
/** Yenileme sonrası bir kez okunur; yeni sürüm numarasını döner. */
export const consumeAppUpdatedVersion = (): string | undefined => {
try {
const response = await fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store' })
if (!response.ok) throw new Error(`Version request failed with ${response.status}`)
const releases = ((await response.json()) as { releases?: Release[] }).releases
latestRelease = releases?.[0]
uiVersion = latestRelease?.version ?? uiVersion
} catch (error) {
console.warn('Update details could not be loaded.', error)
const value = sessionStorage.getItem(UPDATED_VERSION_KEY)
if (value) sessionStorage.removeItem(UPDATED_VERSION_KEY)
return value ?? undefined
} catch {
return undefined
}
// The overlay may have been removed while version.json was loading.
if (!overlay.isConnected) return
const uiValue = overlay.querySelector<HTMLElement>('[data-sw-ui-version]')
const releaseTitle = overlay.querySelector<HTMLElement>('[data-sw-release-title]')
const releaseList = overlay.querySelector<HTMLUListElement>('[data-sw-release-list]')
if (uiValue) uiValue.textContent = `${uiEnvironment}:${uiVersion ?? '-'}`
watchOverlayApiVersion(overlay)
if (!releaseList || !releaseTitle) return
const changes = latestRelease?.changeLog?.filter(Boolean) ?? []
releaseTitle.textContent = latestRelease?.version
? `What's new in v${latestRelease.version}`
: "What's new"
releaseList.replaceChildren()
if (changes.length === 0) {
const item = document.createElement('li')
item.className = 'sw-update-empty'
item.textContent = 'Release notes are not available.'
releaseList.appendChild(item)
return
}
changes.forEach((change) => {
const item = document.createElement('li')
const icon = document.createElement('span')
const text = document.createElement('span')
icon.className = 'sw-update-check'
icon.setAttribute('aria-hidden', 'true')
icon.textContent = '\u2713'
text.textContent = change.replace(/^\s*-\s*/, '')
item.append(icon, text)
releaseList.appendChild(item)
})
}
function showUpdateOverlay() {
if (document.getElementById('sw-update-overlay')) return
/* ------------------------------------------------------------------ */
/* Arayüz */
/* ------------------------------------------------------------------ */
const STYLE_ID = 'sw-update-style'
const BANNER_ID = 'sw-update-banner'
const OVERLAY_ID = 'sw-update-overlay'
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return
const style = document.createElement('style')
style.id = 'sw-update-overlay-style'
style.id = STYLE_ID
style.textContent = `
#sw-update-overlay {
#${BANNER_ID} {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 99998;
display: flex;
align-items: center;
gap: 10px;
max-width: 320px;
padding: 12px 16px;
border-radius: 12px;
background: #111827;
color: #f9fafb;
font-size: 13px;
line-height: 1.4;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
#${OVERLAY_ID} {
position: fixed;
inset: 0;
z-index: 99999;
@ -127,7 +137,7 @@ function showUpdateOverlay() {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
#sw-update-overlay .sw-update-card {
.sw-update-card {
display: flex;
flex-direction: column;
align-items: center;
@ -141,7 +151,8 @@ function showUpdateOverlay() {
max-height: min(90vh, 720px);
width: 90%;
}
#sw-update-overlay .sw-update-spinner {
.sw-update-spinner {
flex: 0 0 auto;
width: 56px;
height: 56px;
border: 5px solid #e5e7eb;
@ -149,21 +160,17 @@ function showUpdateOverlay() {
border-radius: 50%;
animation: sw-spin 0.8s linear infinite;
}
@keyframes sw-spin {
to { transform: rotate(360deg); }
#${BANNER_ID} .sw-update-spinner {
width: 20px;
height: 20px;
border-width: 3px;
border-color: rgba(255,255,255,0.25);
border-top-color: #a5b4fc;
}
#sw-update-overlay .sw-update-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
#sw-update-overlay .sw-update-desc {
font-size: 14px;
color: #6b7280;
margin: 0;
}
#sw-update-overlay .sw-update-versions {
@keyframes sw-spin { to { transform: rotate(360deg); } }
.sw-update-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
.sw-update-desc { font-size: 14px; color: #6b7280; margin: 0; }
.sw-update-versions {
display: flex;
flex-wrap: wrap;
align-items: center;
@ -175,13 +182,9 @@ function showUpdateOverlay() {
background: #f3f4f6;
color: #4b5563;
font-size: 13px;
text-transform: capitalize;
}
#sw-update-overlay .sw-update-separator {
color: #9ca3af;
margin: 0 4px;
}
#sw-update-overlay .sw-update-release {
.sw-update-separator { color: #9ca3af; margin: 0 4px; }
.sw-update-release {
width: 100%;
min-height: 0;
overflow-y: auto;
@ -189,13 +192,8 @@ function showUpdateOverlay() {
border-top: 1px solid #e5e7eb;
padding-top: 14px;
}
#sw-update-overlay .sw-update-release-title {
margin: 0 0 10px;
color: #374151;
font-size: 14px;
font-weight: 700;
}
#sw-update-overlay .sw-update-list {
.sw-update-release-title { margin: 0 0 10px; color: #374151; font-size: 14px; font-weight: 700; }
.sw-update-list {
display: flex;
flex-direction: column;
gap: 8px;
@ -206,12 +204,8 @@ function showUpdateOverlay() {
font-size: 13px;
line-height: 1.45;
}
#sw-update-overlay .sw-update-list li {
display: flex;
align-items: flex-start;
gap: 8px;
}
#sw-update-overlay .sw-update-check {
.sw-update-list li { display: flex; align-items: flex-start; gap: 8px; }
.sw-update-check {
display: inline-flex;
flex: 0 0 16px;
align-items: center;
@ -225,51 +219,189 @@ function showUpdateOverlay() {
font-size: 11px;
font-weight: 700;
}
#sw-update-overlay .sw-update-empty {
display: block;
color: #9ca3af;
.sw-update-empty { display: block; color: #9ca3af; }
.sw-update-action {
appearance: none;
border: 0;
cursor: pointer;
padding: 10px 20px;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 14px;
font-weight: 600;
}
.sw-update-action:hover { background: #4338ca; }
@media (max-width: 640px) {
#sw-update-overlay .sw-update-card {
padding: 24px 20px;
}
.sw-update-card { padding: 24px 20px; }
#${BANNER_ID} { left: 16px; right: 16px; max-width: none; }
}
`
document.head.appendChild(style)
}
function removeStyles() {
document.getElementById(STYLE_ID)?.remove()
}
/** İndirme sürerken kullanıcıyı engellemeyen bilgilendirme. */
function showDownloadBanner() {
if (document.getElementById(BANNER_ID) || document.getElementById(OVERLAY_ID)) return
ensureStyles()
const banner = document.createElement('div')
banner.id = BANNER_ID
banner.setAttribute('role', 'status')
banner.setAttribute('aria-live', 'polite')
const spinner = document.createElement('div')
spinner.className = 'sw-update-spinner'
const text = document.createElement('span')
text.textContent = 'Yeni sürüm indiriliyor, lütfen sekmeyi kapatmayın...'
banner.append(spinner, text)
document.body.appendChild(banner)
}
function removeDownloadBanner() {
document.getElementById(BANNER_ID)?.remove()
}
function getText(value: unknown) {
return value === undefined || value === null ? undefined : String(value)
}
function applyApiVersion(overlay: HTMLElement) {
const apiConfig = store.getState().abpConfig.config?.extraProperties
const environment = getText(apiConfig?.environment)
const version = getText(apiConfig?.version)
if (!environment && !version) return false
const target = overlay.querySelector<HTMLElement>('[data-sw-api-version]')
if (target) target.textContent = `${environment ?? '-'}:${version ?? '-'}`
return true
}
function watchApiVersion(overlay: HTMLElement) {
storeUnsubscribe?.()
storeUnsubscribe = undefined
if (applyApiVersion(overlay)) return
storeUnsubscribe = store.subscribe(() => {
if (!overlay.isConnected || applyApiVersion(overlay)) {
storeUnsubscribe?.()
storeUnsubscribe = undefined
}
})
}
async function fillOverlayDetails(overlay: HTMLElement) {
const release = await loadLatestRelease()
if (!overlay.isConnected) return
const uiVersion = release?.version ?? store.getState().locale.currentUiVersion
const uiValue = overlay.querySelector<HTMLElement>('[data-sw-ui-version]')
if (uiValue) uiValue.textContent = `${import.meta.env.MODE}:${uiVersion ?? '-'}`
watchApiVersion(overlay)
const releaseTitle = overlay.querySelector<HTMLElement>('[data-sw-release-title]')
const releaseList = overlay.querySelector<HTMLUListElement>('[data-sw-release-list]')
if (!releaseTitle || !releaseList) return
releaseTitle.textContent = release?.version
? `v${release.version} ile gelen yenilikler`
: 'Sürüm notları'
releaseList.replaceChildren()
const changes = release?.changeLog?.filter(Boolean) ?? []
if (changes.length === 0) {
const item = document.createElement('li')
item.className = 'sw-update-empty'
item.textContent = 'Bu sürüm için not bulunamadı.'
releaseList.appendChild(item)
return
}
changes.forEach((change) => {
const item = document.createElement('li')
const icon = document.createElement('span')
const text = document.createElement('span')
icon.className = 'sw-update-check'
icon.setAttribute('aria-hidden', 'true')
icon.textContent = '✓'
text.textContent = change.replace(/^\s*-\s*/, '')
item.append(icon, text)
releaseList.appendChild(item)
})
}
function showActivationOverlay() {
removeDownloadBanner()
if (document.getElementById(OVERLAY_ID)) return
ensureStyles()
const overlay = document.createElement('div')
overlay.id = 'sw-update-overlay'
overlay.id = OVERLAY_ID
overlay.setAttribute('role', 'status')
overlay.setAttribute('aria-live', 'polite')
overlay.innerHTML = `
<div class="sw-update-card">
<div class="sw-update-spinner"></div>
<p class="sw-update-title">System Updating</p>
<p class="sw-update-desc">Loading new version, please wait...<br/>The page will reload automatically.</p>
<p class="sw-update-title" data-sw-title>Sistem güncelleniyor</p>
<p class="sw-update-desc" data-sw-desc>Yeni sürüm yükleniyor, lütfen bekleyin...<br/>Sayfa otomatik olarak yenilenecek.</p>
<div class="sw-update-versions">
<span><strong>UI:</strong> <span data-sw-ui-version>Loading...</span></span>
<span><strong>UI:</strong> <span data-sw-ui-version>Yükleniyor...</span></span>
<span class="sw-update-separator">|</span>
<span><strong>API:</strong> <span data-sw-api-version>Loading...</span></span>
<span><strong>API:</strong> <span data-sw-api-version>Yükleniyor...</span></span>
</div>
<div class="sw-update-release">
<p class="sw-update-release-title" data-sw-release-title>What's new</p>
<p class="sw-update-release-title" data-sw-release-title>Sürüm notları</p>
<ul class="sw-update-list" data-sw-release-list>
<li class="sw-update-empty">Loading release notes...</li>
<li class="sw-update-empty">Sürüm notları yükleniyor...</li>
</ul>
</div>
</div>
`
document.body.appendChild(overlay)
void loadUpdateOverlayDetails(overlay)
void fillOverlayDetails(overlay)
}
function hideUpdateOverlay() {
overlayStoreUnsubscribe?.()
overlayStoreUnsubscribe = undefined
document.getElementById('sw-update-overlay')?.remove()
document.getElementById('sw-update-overlay-style')?.remove()
/** Aktivasyon tamamlanamazsa kullanıcıyı belirsizlikte bırakma. */
function showManualReloadPrompt() {
const overlay = document.getElementById(OVERLAY_ID)
if (!overlay) return
overlay.querySelector<HTMLElement>('.sw-update-spinner')?.remove()
const title = overlay.querySelector<HTMLElement>('[data-sw-title]')
const desc = overlay.querySelector<HTMLElement>('[data-sw-desc]')
if (title) title.textContent = 'Güncelleme tamamlanamadı'
if (desc) {
desc.textContent = 'Yeni sürüme geçmek için sayfayı yenileyin.'
}
if (overlay.querySelector('.sw-update-action')) return
const button = document.createElement('button')
button.type = 'button'
button.className = 'sw-update-action'
button.textContent = 'Sayfayı yenile'
button.addEventListener('click', () => reloadForUpdate())
overlay.querySelector('.sw-update-card')?.appendChild(button)
}
function teardownUpdateUi() {
storeUnsubscribe?.()
storeUnsubscribe = undefined
removeDownloadBanner()
document.getElementById(OVERLAY_ID)?.remove()
removeStyles()
}
/* ------------------------------------------------------------------ */
/* Aktivasyon */
/* ------------------------------------------------------------------ */
function clearActivationTimers() {
window.clearTimeout(activationRetryTimer)
window.clearTimeout(activationTimeoutTimer)
@ -278,189 +410,283 @@ function clearActivationTimers() {
}
function reloadForUpdate() {
if (reloadStarted) return
reloadStarted = true
if (reloading) return
reloading = true
clearActivationTimers()
try {
if (latestVersion) sessionStorage.setItem(UPDATED_VERSION_KEY, latestVersion)
} catch {
/* sessionStorage kullanılamıyorsa bildirim atlanır */
}
window.location.reload()
}
function activateWaitingWorker(
registration: ServiceWorkerRegistration | undefined,
activateUpdate: (reloadPage?: boolean) => Promise<void>,
showOverlay: boolean,
) {
if (activationInProgress) {
if (showOverlay) showUpdateOverlay()
function postSkipWaiting() {
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
}
function activateWaitingWorker() {
if (activating) {
showActivationOverlay()
return
}
activationInProgress = true
activating = true
showActivationOverlay()
// Sürüm numarasını yenileme öncesinde hazır tut.
void loadLatestRelease()
if (showOverlay) {
showUpdateOverlay()
}
navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, { once: true })
postSkipWaiting()
// Workbox'ın controlling olayı herhangi bir nedenle kaçarsa native olay da
// yeni worker kontrolü aldığı anda sayfayı yeniler.
navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, {
once: true,
})
// Bazı tarayıcılarda ilk mesaj bekleyen worker'a ulaşmayabiliyor.
activationRetryTimer = window.setTimeout(postSkipWaiting, ACTIVATION_RETRY_DELAY)
// Workbox callback sırasına bağlı kalmadan waiting worker'ı doğrudan geçir.
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
void activateUpdate(true).catch((error: unknown) => {
console.error('Service worker activation failed.', error)
activationInProgress = false
clearActivationTimers()
hideUpdateOverlay()
})
// Bazı tarayıcılarda Workbox mesajı bekleyen worker'a ulaşmayabiliyor.
// Kısa bir süre sonra native mesajla bir kez daha aktivasyon istenir.
activationRetryTimer = window.setTimeout(() => {
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
}, ACTIVATION_RETRY_DELAY)
// Aktivasyon yine de tamamlanamazsa uygulamayı sonsuza kadar kilitleme.
// Eski worker çalışmaya devam eder; bir sonraki açılışta tekrar denenir.
activationTimeoutTimer = window.setTimeout(() => {
activationInProgress = false
clearActivationTimers()
activating = false
// Worker waiting durumundan çıktıysa aktivasyon gerçekleşmiş fakat
// controllerchange olayı kaçmış olabilir. Yeni precache'i almak için yenile.
// Worker waiting'den çıktıysa aktivasyon olmuş, sadece controllerchange
// kaçmış olabilir; yeni precache'i almak için yenile.
if (!registration?.waiting && registration?.active) {
reloadForUpdate()
return
}
hideUpdateOverlay()
console.warn('Service worker activation timed out; continuing with current version.')
console.warn('Service worker aktivasyonu zaman aşımına uğradı.')
showManualReloadPrompt()
}, ACTIVATION_TIMEOUT)
}
function showUpdateWhileInstalling(registration: ServiceWorkerRegistration) {
const installingWorker = registration.installing
if (
!installingWorker ||
!navigator.serviceWorker.controller ||
watchedInstallingWorkers.has(installingWorker)
) {
return
}
/* ------------------------------------------------------------------ */
/* Güncelleme takibi */
/* ------------------------------------------------------------------ */
watchedInstallingWorkers.add(installingWorker)
showUpdateOverlay()
function trackInstallingWorker(reg: ServiceWorkerRegistration) {
const worker = reg.installing
// Controller yoksa bu ilk kurulumdur; kullanıcıya güncelleme demek yanlış olur.
if (!worker || !navigator.serviceWorker.controller || watchedWorkers.has(worker)) return
installingWorker.addEventListener('statechange', () => {
// Precache dosyalarından biri indirilemezse worker redundant olur. Eski
// uygulama çalışmaya devam eder ve kullanıcı kilitli ekranda bırakılmaz.
if (installingWorker.state === 'redundant' && !activationInProgress) {
hideUpdateOverlay()
watchedWorkers.add(worker)
showDownloadBanner()
worker.addEventListener('statechange', () => {
if (worker.state === 'installed') {
activateWaitingWorker()
return
}
// Precache dosyalarından biri indirilemezse worker redundant olur; eski
// sürüm çalışmaya devam eder ve kullanıcı kilitli ekranda kalmaz.
if (worker.state === 'redundant' && !activating) {
console.warn('Yeni service worker kurulamadı, mevcut sürümle devam ediliyor.')
teardownUpdateUi()
}
})
}
function watchForUpdate(registration: ServiceWorkerRegistration) {
registration.addEventListener('updatefound', () => {
// Gerçek worker güncellemesi bulunduğu anda, precache indirmesi başlamadan
// Loading'i göster ve aktivasyon/reload tamamlanana kadar açık tut.
showUpdateWhileInstalling(registration)
})
// Listener bağlanmadan hemen önce başlamış bir kurulumu da kaçırma.
showUpdateWhileInstalling(registration)
function watchRegistration(reg: ServiceWorkerRegistration) {
if (watchedRegistrations.has(reg)) return
watchedRegistrations.add(reg)
reg.addEventListener('updatefound', () => trackInstallingWorker(reg))
trackInstallingWorker(reg)
}
function checkForUpdate(registration: ServiceWorkerRegistration) {
// Bir deploy versiyon numarası/tag değişmeden yapılsa bile sw.js içindeki
// precache manifest'i değişir. Uygulama açılırken yapılan bu tek kontrol
// yeni build'i algılar; açık uygulamada periyodik bir arka plan işi çalışmaz.
void registration.update().catch((error: unknown) => {
console.warn('Service worker update check failed.', error)
/**
* Kayıtlı worker'ın script'i artık sunulmuyorsa (ör. PWA kapatıldı, dosya adı
* değişti) sunucu SPA fallback'i yüzünden index.html döner ve tarayıcı
* "unsupported MIME type" SecurityError'ı fırlatır. Bu durumda eski kayıt
* kalıcı olarak bozuktur; temizlenmezse her kontrol aynı hatayı verir.
*/
async function unregisterIfStale(reg: ServiceWorkerRegistration, error: unknown) {
const isStaleScript =
error instanceof Error && (error.name === 'SecurityError' || /MIME type/i.test(error.message))
if (!isStaleScript) return false
console.warn('Geçersiz service worker kaydı temizleniyor.', error)
try {
await reg.unregister()
} catch {
/* temizlenemediyse bir sonraki açılışta tekrar denenir */
}
if (registration === reg) registration = undefined
return true
}
async function runUpdateCheck(force = false) {
if (!registration || activating || reloading) return
if (!force && !navigator.onLine) return
const now = Date.now()
if (!force && now - lastCheckAt < MIN_CHECK_GAP) return
lastCheckAt = now
const reg = registration
try {
await withTimeout(reg.update(), UPDATE_CALL_TIMEOUT)
} catch (error: unknown) {
if (!(await unregisterIfStale(reg, error))) {
console.warn('Service worker güncelleme kontrolü başarısız.', error)
}
}
}
function startUpdateWatchers() {
window.clearInterval(periodicTimer)
periodicTimer = window.setInterval(() => {
if (document.visibilityState === 'visible') void runUpdateCheck()
}, PERIODIC_CHECK_INTERVAL)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') void runUpdateCheck()
})
window.addEventListener('online', () => void runUpdateCheck())
}
/**
* Deploy sonrası sunucudan kaldırılmış bir lazy chunk istenirse Vite
* `vite:preloadError` fırlatır. Beyaz ekran yerine bir kez sayfayı yenile.
*/
function handleChunkLoadFailures() {
window.addEventListener('vite:preloadError', (event) => {
if (reloading) return
try {
if (sessionStorage.getItem(CHUNK_RELOAD_GUARD_KEY)) return
sessionStorage.setItem(CHUNK_RELOAD_GUARD_KEY, '1')
} catch {
return
}
event.preventDefault()
console.warn('Eksik chunk tespit edildi, sayfa yenileniyor.')
reloading = true
window.location.reload()
})
// Sayfa sorunsuz açıldıysa koruma bayrağını temizle.
window.setTimeout(() => {
try {
sessionStorage.removeItem(CHUNK_RELOAD_GUARD_KEY)
} catch {
/* yoksay */
}
}, 10_000)
}
/* ------------------------------------------------------------------ */
/* Genel API */
/* ------------------------------------------------------------------ */
/** Service worker yalnızca prod'da, dev'de ise VITE_PWA_DEV=true ile aktiftir. */
const isServiceWorkerEnabled = import.meta.env.PROD || import.meta.env.VITE_PWA_DEV === 'true'
/** Kapalıyken önceki oturumlardan kalan kayıtları temizler. */
async function unregisterAll() {
try {
const registrations = await navigator.serviceWorker.getRegistrations()
await Promise.all(registrations.map((reg) => reg.unregister().catch(() => false)))
} catch {
/* yoksay */
}
}
export const registerServiceWorker = () => {
if (registrationStarted || !('serviceWorker' in navigator)) return
registrationStarted = true
if (started || !('serviceWorker' in navigator)) return
started = true
registrationInitialization = navigator.serviceWorker
.getRegistration()
.catch(() => undefined)
.then((existingRegistration) => {
if (existingRegistration) {
watchForUpdate(existingRegistration)
}
handleChunkLoadFailures()
const activateUpdate = registerSW({
immediate: true,
onRegisteredSW(_swUrl, registration) {
if (!registration) return
if (!isServiceWorkerEnabled) {
// Dev'de SW kapalı. Daha önce kaydedilmiş dev worker'ı bırakırsak script'i
// (/dev-sw.js) artık üretilmediği için sunucu index.html döner ve her
// güncelleme kontrolü "unsupported MIME type" hatası verir.
initialization = unregisterAll()
return
}
if (registration !== existingRegistration) {
watchForUpdate(registration)
initialization = new Promise<void>((resolve) => {
// Kayıt callback'i hiç çalışmazsa bile promise'i serbest bırak; aksi halde
// `checkForAppUpdate()` "Kontrol ediliyor..." durumunda takılı kalır.
const registrationTimeout = window.setTimeout(resolve, REGISTRATION_TIMEOUT)
const settle = () => {
window.clearTimeout(registrationTimeout)
resolve()
}
registerSW({
immediate: true,
onRegisteredSW(_swUrl, reg) {
if (reg) {
registration = reg
watchRegistration(reg)
startUpdateWatchers()
// Kurulumu tamamlanmış, aktif olandan farklı bir worker bekliyorsa
// doğrudan devreye al.
if (reg.waiting && navigator.serviceWorker.controller) {
activateWaitingWorker()
} else {
void runUpdateCheck(true)
}
currentRegistration = registration
checkForUpdate(registration)
// waiting yalnızca aktif worker'dan farklı, kurulumu tamamlanmış gerçek
// bir worker olduğunda bulunur. Onu doğrudan etkinleştir.
if (registration.waiting && navigator.serviceWorker.controller) {
activateWaitingWorker(registration, activateUpdate, true)
}
},
onNeedRefresh() {
// Bu callback worker'ın bütün yeni JS/CSS dosyalarını başarıyla precache'e
// almasından sonra çalışır; aktivasyon ve reload atomik yapılır.
activateWaitingWorker(currentRegistration, activateUpdate, true)
},
onNeedReload() {
reloadForUpdate()
},
onOfflineReady() {
console.log('📦 App offline ready')
},
onRegisterError(error) {
activationInProgress = false
clearActivationTimers()
hideUpdateOverlay()
console.error('Service worker registration failed.', error)
},
})
activateUpdateHandler = activateUpdate
}
settle()
},
onNeedRefresh() {
// Yeni worker tüm kabuk dosyalarını precache'e aldıktan sonra çalışır.
activateWaitingWorker()
},
onRegisterError(error) {
activating = false
clearActivationTimers()
teardownUpdateUi()
console.error('Service worker kaydı başarısız.', error)
settle()
},
})
})
}
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
if (!('serviceWorker' in navigator)) return 'unsupported'
if (!isServiceWorkerEnabled) return 'disabled'
// App içindeki normal kayıt işlemi henüz bitmediyse önce onu tamamla.
await registrationInitialization
const registration =
currentRegistration ?? (await navigator.serviceWorker.ready.catch(() => undefined))
await initialization
// `navigator.serviceWorker.ready` kayıt yokken (ör. dev ortamı) hiçbir zaman
// resolve etmez; getRegistration() kayıt yoksa undefined döner.
const reg =
registration ?? (await navigator.serviceWorker.getRegistration().catch(() => undefined))
if (!reg) return 'unsupported'
if (!registration) return 'unsupported'
registration ??= reg
watchRegistration(reg)
if (reg.waiting && navigator.serviceWorker.controller) {
activateWaitingWorker()
return 'updating'
}
let updateFound = false
const handleUpdateFound = () => {
updateFound = true
}
registration.addEventListener('updatefound', handleUpdateFound)
let staleRegistrationCleared = false
reg.addEventListener('updatefound', handleUpdateFound)
try {
await registration.update()
await withTimeout(reg.update(), UPDATE_CALL_TIMEOUT)
} catch (error: unknown) {
staleRegistrationCleared = await unregisterIfStale(reg, error)
if (!staleRegistrationCleared) {
console.warn('Manuel güncelleme kontrolü başarısız.', error)
}
} finally {
registration.removeEventListener('updatefound', handleUpdateFound)
reg.removeEventListener('updatefound', handleUpdateFound)
lastCheckAt = Date.now()
}
const updateAvailable = Boolean(updateFound || registration.installing || registration.waiting)
if (!updateAvailable) return 'up-to-date'
// Bozuk kayıt temizlendi; bir sonraki açılışta yeni worker kaydedilecek.
if (staleRegistrationCleared) return 'up-to-date'
if (registration.waiting && activateUpdateHandler) {
activateWaitingWorker(registration, activateUpdateHandler, true)
}
if (!updateFound && !reg.installing && !reg.waiting) return 'up-to-date'
// installing durumundaysa gerçek onNeedRefresh callback'i indirme bittikten
// sonra aynı overlay/aktivasyon akışını başlatır.
if (reg.waiting && navigator.serviceWorker.controller) activateWaitingWorker()
// `installing` durumundaysa statechange dinleyicisi aynı akışı sürdürür.
return 'updating'
}

View file

@ -0,0 +1,77 @@
import { useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useStoreActions } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { consumeAppUpdatedVersion } from './swRegistration'
/**
* ılışta çalışan tek sürüm kaynağı:
* - `version.json`'daki güncel sürümü store'a yazar (footer/changelog bunu okur).
* - Service worker güncellemesi sonrası yenilenen sayfada "güncellendi"
* bildirimini gösterip changelog'a yönlendirme sunar.
*
* Daha önce bu işi Login ekranı yapıyordu; store'daki başlangıç sürümü
* `undefined` olduğu için her girişte changelog'a zorla yönlendiriyordu.
*/
export const useAppVersionNotice = () => {
const navigate = useNavigate()
const { translate } = useLocalization()
const setUiVersion = useStoreActions((actions) => actions.locale.setUiVersion)
const handled = useRef(false)
useEffect(() => {
if (handled.current) return
handled.current = true
const label = (key: string, fallback: string) => {
const value = translate(key)
return !value || value === key || value.includes('::') ? fallback : value
}
const updatedVersion = consumeAppUpdatedVersion()
const controller = new AbortController()
fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store', signal: controller.signal })
.then((response) => (response.ok ? response.json() : undefined))
.then(async (data?: { releases?: { version: string }[] }) => {
const latest = data?.releases?.[0]?.version
if (latest) setUiVersion(latest)
if (!updatedVersion) return
// toast/Notification (ve bağımlısı framer-motion) yalnızca güncelleme
// sonrası bu tek seferlik bildirimde gerekiyor; entry chunk'ta taşınmaz.
const [{ default: toast }, { default: Notification }] = await Promise.all([
import('@/components/ui/toast'),
import('@/components/ui/Notification'),
])
toast.push(
<Notification
closable
duration={10000}
title={label('::App.UpdateCompleted', 'Uygulama güncellendi')}
type="success"
>
{label('::App.UpdatedToVersion', 'Yeni sürüm')} v{updatedVersion} {' '}
<button
type="button"
className="underline"
onClick={() => navigate(ROUTES_ENUM.protected.admin.changeLog)}
>
{label('::App.ChangeLog', 'Değişiklikleri gör')}
</button>
</Notification>,
{ placement: 'bottom-end' },
)
})
.catch(() => {
/* sürüm bilgisi okunamadıysa sessiz geç */
})
return () => controller.abort()
}, [])
}
export default useAppVersionNotice

View file

@ -7,6 +7,9 @@ interface ImportMetaEnv {
readonly VITE_REACT_APP_VERSION: string
readonly VITE_AI_URL: string
readonly VITE_GOOGLE_MAPS_API_KEY: string
readonly VITE_USE_POLLING?: string
/** Dev sunucusunda service worker'ı açar (varsayılan kapalı). */
readonly VITE_PWA_DEV?: string
}
interface ImportMeta {

View file

@ -2,16 +2,47 @@ import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { VitePWA } from 'vite-plugin-pwa'
// import { fetchTenantNames } from './src/services/tenants'
export default defineConfig(async ({ mode }) => {
/**
* 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
* ı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 apiUrl = env.VITE_API_URL
const baseDomains = ['sozsoft.com', 'dev.sozsoft.com', '.sozsoft.com']
const tenantHosts: string[] = []
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 allowedHosts = Array.from(new Set(['localhost', ...baseDomains, ...tenantHosts]))
console.log('> [vite] allowedHosts:', allowedHosts)
const baseDomains = ['sozsoft.com', 'dev.sozsoft.com', '.sozsoft.com']
const allowedHosts = Array.from(new Set(['localhost', ...baseDomains]))
return {
plugins: [
@ -20,87 +51,83 @@ export default defineConfig(async ({ mode }) => {
// 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 loading akışı src/views/version/swRegistration.ts tarafından yönetilir.
// Kayıt ve güncelleme akışı src/views/version/swRegistration.ts tarafından yönetilir.
injectRegister: false,
// Dev ortamında SW'yi aç, prod'da kapalı tut (build edilmiş SW prod'da zaten aktif olur)
devOptions: {
enabled: mode !== 'production',
type: 'module', // Modern module worker kullan
enabled: enableDevSw,
type: 'module',
},
workbox:
mode === 'production'
? {
globDirectory: 'dist',
// Eski bir sekme yeni deploy sırasında henüz açmadığı lazy
// chunk'ları da çalıştırabilsin; tüm uygulama dosyalarını sakla.
globPatterns: ['**/*.{js,css,html,wasm}'],
workbox: {
globDirectory: 'dist',
globPatterns: PRECACHE_GLOBS,
// Büyük asset'leri de cache'leyebil
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
// Kabuk dosyaları büyük olabilir (entry CSS ~1.3 MB).
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
// Yeni worker tüm precache dosyalarını indirdikten sonra uygulama
// SKIP_WAITING mesajını gönderir. clientsClaim açık sekmeleri o anda yeniler.
clientsClaim: true,
skipWaiting: false,
// 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,
// Eski workbox cache'lerini temizle
cleanupOutdatedCaches: true,
navigateFallback: '/index.html',
navigateFallbackAllowlist: [/^(?!\/__).*/],
navigateFallbackDenylist: [/^\/api\//, /^\/connect\//, /^\/swagger/, /\/version\.json$/],
// SPA fallback'i API çağrılarına uygulama
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: /\.(?:js|css|wasm)$/,
// Hash'li build dosyaları immutable'dır. Normal kullanımda ağ
// beklenmez; deploy güncellemesini precache manifest'i yönetir.
handler: 'CacheFirst',
options: {
cacheName: 'static-resources-v2',
expiration: {
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
// Changelog/versiyon bilgisi cache yüzünden eski kalmamalı.
urlPattern: /\/version\.json$/,
handler: 'NetworkFirst',
options: {
cacheName: 'app-version-v1',
networkTimeoutSeconds: 3,
expiration: {
maxEntries: 5,
maxAgeSeconds: 24 * 60 * 60,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 gün
},
},
},
],
// ⭐ YENİ EKLENEN: Additional navigation route for SPA
navigateFallback: '/index.html',
navigateFallbackAllowlist: [/^(?!\/__).*/],
}
: undefined,
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',
@ -137,10 +164,14 @@ export default defineConfig(async ({ mode }) => {
server: {
open: true,
port: 3000,
// ⭐ YENİ EKLENEN: Hot reload için polling
watch: {
usePolling: env.VITE_USE_POLLING === 'true',
interval: env.VITE_USE_POLLING === 'true' ? 1000 : undefined,
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'],
},
},
@ -154,24 +185,85 @@ export default defineConfig(async ({ mode }) => {
},
},
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: {
entryFileNames: `assets/js/[name]-[hash].js`,
chunkFileNames: `assets/js/[name]-[hash].js`,
assetFileNames: ({ name }) => {
if (/\.(css)$/.test(name ?? '')) {
return 'assets/css/[name]-[hash][extname]'
}
// `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
},
},
},
},
@ -180,16 +272,11 @@ export default defineConfig(async ({ mode }) => {
host: '0.0.0.0',
port: 80,
open: false,
// Hesaplanan allowedHosts'u kullan
allowedHosts,
},
define: {
'process.env': {},
// ⭐ YENİ EKLENEN: Version tracking için global değişkenler
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || '1.0.0'),
__BUILD_DATE__: JSON.stringify(new Date().toISOString()),
__APP_MODE__: JSON.stringify(mode),
},
}
})