import { cpSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { join, resolve } from 'path'; import { fileURLToPath } from 'url'; import { defineConfig, type ResolvedConfig, type ViteDevServer } from 'vite'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; const DEV_APP_UPDATE_PATH = '/__app-update'; const processEnv = ( globalThis as typeof globalThis & { process?: { env?: Record; }; } ).process?.env ?? {}; const VITE_PUBLIC_HMR_HOST = processEnv.VITE_PUBLIC_HMR_HOST ?? 'test.sm-home.cloud'; const VITE_PUBLIC_HMR_PROTOCOL = processEnv.VITE_PUBLIC_HMR_PROTOCOL ?? 'wss'; const VITE_PUBLIC_HMR_CLIENT_PORT = Number(processEnv.VITE_PUBLIC_HMR_CLIENT_PORT ?? 443); const VITE_EMPTY_OUT_DIR = processEnv.VITE_EMPTY_OUT_DIR !== 'false'; const VITE_FILTER_PUBLIC_DIR = processEnv.VITE_FILTER_PUBLIC_DIR === 'true'; const VITE_DISABLE_MODULE_PRELOAD = processEnv.VITE_DISABLE_MODULE_PRELOAD === 'true'; const VITE_DISABLE_PWA = processEnv.VITE_DISABLE_PWA === 'true'; const WORK_SERVER_HTTP_TARGET = processEnv.WORK_SERVER_URL?.trim() || 'http://work-server:3100'; const WORK_SERVER_WS_TARGET = (() => { try { const parsed = new URL(WORK_SERVER_HTTP_TARGET); parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; return parsed.toString(); } catch { return WORK_SERVER_HTTP_TARGET.replace(/^http/i, 'ws'); } })(); const ROOT_DIR = fileURLToPath(new URL('.', import.meta.url)); function shouldIgnoreDevUpdatePath(watchedPath: string) { return ( watchedPath.includes('/.auto_codex/') || watchedPath.includes('/dist/') || watchedPath.includes('/app-dist/') || watchedPath.includes('/test-app-dist/') || watchedPath.includes('/node_modules/') || watchedPath.includes('/public/.codex_chat/') || watchedPath.endsWith('/.server-command-runner-heartbeat.json') ); } function createDevAppUpdatePlugin() { let updateToken = `${Date.now()}`; const bumpUpdateToken = () => { updateToken = `${Date.now()}`; }; const shouldTrackPath = (watchedPath: string) => !shouldIgnoreDevUpdatePath(watchedPath); return { name: 'dev-app-update-probe', apply: 'serve' as const, configureServer(server: ViteDevServer) { const handleFileChange = (file: string) => { if (shouldTrackPath(file)) { bumpUpdateToken(); } }; server.watcher.on('add', handleFileChange); server.watcher.on('change', handleFileChange); server.watcher.on('unlink', handleFileChange); server.middlewares.use(DEV_APP_UPDATE_PATH, (_request, response) => { response.setHeader('Content-Type', 'application/json; charset=utf-8'); response.setHeader('Cache-Control', 'no-store'); response.end( JSON.stringify({ token: updateToken, updatedAt: new Date(Number(updateToken)).toISOString(), }), ); }); }, }; } function createDevServiceWorkerPlugin() { return { name: 'dev-service-worker-entry', apply: 'serve' as const, configureServer(server: ViteDevServer) { server.middlewares.use((request, response, next) => { const requestUrl = request.url?.trim() ?? ''; const requestPath = requestUrl.split('?', 1)[0]; if (requestPath !== '/sw.js' && requestPath !== '/dev-sw.js') { next(); return; } void server .transformRequest('/src/sw.js') .then((result) => { if (!result?.code) { response.statusCode = 404; response.setHeader('Content-Type', 'text/plain; charset=utf-8'); response.end('Not Found'); return; } response.statusCode = 200; response.setHeader('Content-Type', 'application/javascript; charset=utf-8'); response.setHeader('Cache-Control', 'no-store'); response.setHeader('Service-Worker-Allowed', '/'); response.end(result.code); }) .catch(next); }); }, }; } function copyPublicAssetsExceptCodexChat() { let resolvedConfig: ResolvedConfig | null = null; const copyDirectory = (sourceDir: string, targetDir: string) => { if (!existsSync(sourceDir)) { return; } mkdirSync(targetDir, { recursive: true }); for (const entry of readdirSync(sourceDir, { withFileTypes: true })) { if (entry.name === '.codex_chat') { continue; } const sourcePath = join(sourceDir, entry.name); const targetPath = join(targetDir, entry.name); if (entry.isDirectory()) { copyDirectory(sourcePath, targetPath); continue; } if (entry.isFile()) { mkdirSync(join(targetPath, '..'), { recursive: true }); cpSync(sourcePath, targetPath, { force: true }); } } }; return { name: 'copy-public-assets-except-codex-chat', apply: 'build' as const, configResolved(config: ResolvedConfig) { resolvedConfig = config; }, closeBundle() { if (!VITE_FILTER_PUBLIC_DIR || !resolvedConfig) { return; } const publicDir = join(resolvedConfig.root, 'public'); const outDir = resolvedConfig.build.outDir.startsWith('/') ? resolvedConfig.build.outDir : join(resolvedConfig.root, resolvedConfig.build.outDir); copyDirectory(publicDir, outDir); }, }; } export default defineConfig({ resolve: { extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx', '.mts', '.json'], alias: VITE_DISABLE_PWA ? { 'virtual:pwa-register': resolve(ROOT_DIR, 'src/app/main/pwaRegisterStub.ts'), } : undefined, }, build: { copyPublicDir: !VITE_FILTER_PUBLIC_DIR, emptyOutDir: VITE_EMPTY_OUT_DIR, modulePreload: VITE_DISABLE_MODULE_PRELOAD ? false : undefined, }, server: { host: '0.0.0.0', port: 5173, hmr: { host: VITE_PUBLIC_HMR_HOST, protocol: VITE_PUBLIC_HMR_PROTOCOL as 'ws' | 'wss', clientPort: VITE_PUBLIC_HMR_CLIENT_PORT, }, allowedHosts: ['sm-home.cloud', 'test.sm-home.cloud', 'preview.sm-home.cloud', 'rel.sm-home.cloud'], watch: { ignored: (watchedPath) => shouldIgnoreDevUpdatePath(watchedPath), }, proxy: { '/api': { target: WORK_SERVER_HTTP_TARGET, changeOrigin: true, }, '/ws/chat': { target: WORK_SERVER_WS_TARGET, ws: true, changeOrigin: true, }, '/.codex_chat': { target: WORK_SERVER_HTTP_TARGET, changeOrigin: true, }, }, }, preview: { host: '0.0.0.0', port: 5173, allowedHosts: ['sm-home.cloud', 'test.sm-home.cloud', 'preview.sm-home.cloud', 'rel.sm-home.cloud'], }, plugins: [ react(), createDevAppUpdatePlugin(), createDevServiceWorkerPlugin(), copyPublicAssetsExceptCodexChat(), !VITE_DISABLE_PWA && VitePWA({ injectRegister: null, strategies: 'injectManifest', srcDir: 'src', filename: 'sw.js', registerType: 'prompt', includeAssets: ['favicon.svg', 'apple-touch-icon.svg'], workbox: { globPatterns: ['**/*.{js,css,html,ico,svg,woff2,webmanifest}'], maximumFileSizeToCacheInBytes: 8 * 1024 * 1024, }, injectManifest: { globIgnores: ['**/.codex_chat/**', '**/*.png', '**/*.webp'], maximumFileSizeToCacheInBytes: 8 * 1024 * 1024, }, manifest: { id: '/', name: 'AI Code App', short_name: 'AI Code App', description: 'Ant Design 기반 UI 샘플과 문서를 확인하는 AI Code App', theme_color: '#165dff', background_color: '#eff5ff', display: 'standalone', lang: 'ko', scope: '/', start_url: '/', icons: [ { src: '/pwa-192x192.svg', sizes: '192x192', type: 'image/svg+xml', }, { src: '/pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable', }, ], shortcuts: [ { name: 'E-Reader', short_name: 'E-Reader', description: '인터넷 기사와 웹 콘텐츠를 전자책처럼 읽습니다.', url: '/play/apps?app=e-reader', icons: [ { src: '/pwa-192x192.svg', sizes: '192x192', type: 'image/svg+xml', }, ], }, ], }, devOptions: { enabled: false, }, }), ].filter(Boolean), });