import { cpSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { join } from 'path'; 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'; 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 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({ 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', 'rel.sm-home.cloud'], watch: { ignored: (watchedPath) => shouldIgnoreDevUpdatePath(watchedPath), }, proxy: { '/api': { target: 'http://work-server:3100', changeOrigin: true, }, '/ws/chat': { target: 'ws://work-server:3100', ws: true, changeOrigin: true, }, '/.codex_chat': { target: 'http://work-server:3100', changeOrigin: true, }, }, }, preview: { host: '0.0.0.0', port: 5173, allowedHosts: ['sm-home.cloud', 'test.sm-home.cloud', 'rel.sm-home.cloud'], }, plugins: [ react(), createDevAppUpdatePlugin(), copyPublicAssetsExceptCodexChat(), VitePWA({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.js', registerType: 'prompt', includeAssets: ['favicon.svg', 'apple-touch-icon.svg'], injectManifest: { globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff2}'], globIgnores: ['**/.codex_chat/**'], maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, }, manifest: { 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', }, ], }, devOptions: { enabled: true, }, }), ], });