import { rmSync } from 'node:fs'; import path from 'node:path'; import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import postcss from 'postcss'; import { defineConfig, loadEnv, type Plugin } from 'vite'; const RETIRED_TEMPLATE_CSS_PATTERN = /(?:baby-object|bark-battle|big-fish|child-motion|creation-(?:agent|work)|creative-agent|custom-world|fusion-pixel|jump-hop|match3d|pixel|platform-recommend|platform-work-detail|public-work|puzzle|rpg|square-hole|story-top-tabs|unified-creation|visual-novel|wooden-fish)/iu; const RETIRED_COMPONENT_MODULE_DIRECTORIES = new Set([ 'asset-studio', 'bark-battle-creation', 'big-fish-creation', 'big-fish-result', 'big-fish-runtime', 'child-motion-demo', 'creation-agent', 'creative-agent', 'custom-world-agent', 'custom-world-home', 'edutainment-creation', 'edutainment-result', 'edutainment-runtime', 'game-canvas', 'jump-hop-result', 'jump-hop-runtime', 'match3d-result', 'match3d-runtime', 'puzzle-clear-creation', 'puzzle-clear-result', 'puzzle-clear-runtime', 'puzzle-gallery', 'puzzle-result', 'puzzle-runtime', 'rpg-creation-asset-studio', 'rpg-creation-editor', 'rpg-creation-result', 'rpg-entry', 'rpg-runtime-panels', 'rpg-runtime-shell', 'square-hole-creation', 'square-hole-result', 'square-hole-runtime', 'unified-creation', 'visual-novel-creation', 'visual-novel-result', 'visual-novel-runtime', 'wooden-fish-result', 'wooden-fish-runtime', ]); const ACTIVE_TOP_LEVEL_SOURCE_MODULES = new Map>([ [ 'components', new Set([ 'ResolvedAssetAudio.tsx', 'ResolvedAssetImage.tsx', 'ResolvedAssetVideo.tsx', ]), ], [ 'hooks', new Set([ 'useGameSettings.ts', 'useHostNavigationCanGoBack.ts', 'useResolvedAssetReadUrl.ts', ]), ], ['persistence', new Set(['gameSettingsStorage.ts', 'storage.ts'])], ['routing', new Set(['activeAppPageRoutes.ts', 'activeAppRoutes.tsx'])], [ 'services', new Set([ 'activeAppTitle.ts', 'apiClient.ts', 'assetReadUrlService.ts', 'authService.ts', 'clipboard.ts', 'frontendRuntimeConfigService.ts', 'sseStream.ts', ]), ], ]); const RETIRED_SERVICE_MODULE_DIRECTORIES = new Set([ 'bark-battle-creation', 'bark-battle-runtime', 'big-fish-creation', 'big-fish-gallery', 'big-fish-runtime', 'big-fish-works', 'child-motion-demo', 'creation-agent', 'creation-audio', 'creative-agent', 'edutainment-baby-drawing', 'edutainment-baby-object', 'input-devices', 'jump-hop', 'match3d-creation', 'match3d-runtime', 'match3d-works', 'puzzle-agent', 'puzzle-clear', 'puzzle-gallery', 'puzzle-onboarding', 'puzzle-runtime', 'puzzle-works', 'rpg-creation', 'rpg-entry', 'rpg-runtime', 'square-hole-creation', 'square-hole-runtime', 'square-hole-works', 'storyEngine', 'visual-novel-creation', 'visual-novel-runtime', 'visual-novel-works', 'wooden-fish', ]); const RETIRED_HOOK_MODULE_DIRECTORIES = new Set([ 'combat', 'rpg-runtime-story', 'rpg-session', ]); const RETIRED_SOURCE_MODULE_DIRECTORIES = new Set(['data', 'games', 'prompts']); const RETIRED_PLATFORM_ENTRY_MODULE_PATTERN = /\/(?:Platform(?:Draft|EntryCreation|EntryFlowShellImpl|EntryHome|EntryWorld|Error|MobileHome|Profile(?:Generation|Played|Qr|Task)|Task|Work)|barkBattle|platform(?:Creation|Dialog|Draft|Edutainment|EntryCreation|External|Generation|Host|MiniGame|Played|Public|Puzzle|Recommend|Rpg|Selection)|puzzleDraft|usePlatform(?:Creation|Entry))[^/]*\.(?:ts|tsx)$/u; const RETIRED_STANDALONE_MODULE_PATHS = [ '/src/App.test.tsx', '/src/App.tsx', '/src/ChildMotionDemoApp.tsx', '/src/RpgRuntimeApp.tsx', '/src/hooks/useCombatFlow.ts', '/src/hooks/useStoryOptions.ts', '/src/main.tsx', '/src/routing/appPageRoutes.ts', '/src/routing/appRoutes.tsx', '/src/services/ai.ts', '/src/services/appTitle.ts', '/src/services/creationEntryConfigService.ts', '/src/services/creationUrlState.ts', '/src/services/runtimeGuestAuth.ts', '/src/services/runtimeRequest.ts', '/src/services/publicWorkCode.ts', '/src/services/runtimeAudioFeedback.ts', '/src/services/useMocapInput.ts', '/src/services/wechatMiniProgramSubscribe.ts', '/src/types.ts', '/src/uiAssets.ts', ] as const; const RETIRED_STANDALONE_MODULE_PATTERNS = [ /\/src\/[^/]*PlaygroundApp\.tsx$/u, /\/src\/components\/common\/(?:PublishShare|publishShare)[^/]*\.(?:ts|tsx)$/u, /\/src\/components\/platform-entry\/PlatformEntryFlowShellImpl\//u, /\/src\/components\/platform-entry\/platform(?:EntryTypes|Match3DRuntimeProfile)[^/]*\.(?:ts|tsx)$/u, /\/src\/services\/customWorld[^/]*\.(?:ts|tsx)$/u, ] as const; const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none); @source "./active-main.tsx"; @source "./ActiveApp.tsx"; @source "./AuthenticatedApp.tsx"; @source "./components/auth"; @source "./components/common"; @source "./components/creation-home"; @source "./components/image-editor"; @source "./components/project"; @source "./components/platform-entry/PlatformEntryFlowShell.tsx"; @source "./components/platform-entry/PlatformEntryActiveFlowShell.tsx"; @source "./components/platform-entry/PlatformActiveProfileView.tsx"; @source "./components/platform-entry/PlatformProfilePrimitives.tsx"; @source "./components/platform-entry/PlatformProfileApiKeysModal.tsx"; @source "./components/platform-entry/PlatformProfileModalShell.tsx"; @source "./components/platform-entry/PlatformProfileRechargeModal.tsx"; @source "./components/platform-entry/PlatformProfileReferralModal.tsx"; @source "./components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx"; @source "./components/platform-entry/PlatformProfileWalletLedgerModal.tsx"; @source "./components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx"; @source "./editor"; @source "../packages/shared/src/components";`; const RETIRED_PUBLIC_ASSET_PATHS = [ '/audio', '/branding/jump-hop-taonier-character.png', '/branding/mobile-home-welcome-taonier-ip.png', '/anthro-cat-illustrations', '/bark-battle-assets', '/character', '/child-motion-demo', '/creation-type-references', '/edutainment-baby-object', '/fusion-pixel.ttf', '/generated-animations', '/generated-bark-battle-assets', '/generated-big-fish-assets', '/generated-character-drafts', '/generated-characters', '/generated-custom-world-covers', '/generated-custom-world-scenes', '/generated-puzzle-assets', '/generated-qwen-sprites', '/Icons', '/chat.png', '/creation-type-references/jump-hop.webp', '/creation-type-references/match3d.webp', '/match3d-background-references', '/match3d-style-references', '/Pixel Monsters Vol1', '/puzzle-creation-templates', '/scene_bg', '/UI', '/ui-previews', '/visual-novel-style-references', '/wooden-fish', ] as const; const RETIRED_API_PATHS = [ '/api/creation', '/api/creation-entry', '/api/public-works', ] as const; function normalizeViteModuleId(id: string) { return id.split('?', 1)[0]?.replaceAll('\\', '/') ?? id; } const ROOT_SOURCE_DIRECTORY = normalizeViteModuleId( path.resolve(__dirname, 'src'), ); function readTopLevelSourceDirectory( normalizedId: string, sourceKind: 'components' | 'hooks' | 'services', ) { const marker = `/src/${sourceKind}/`; const markerIndex = normalizedId.lastIndexOf(marker); if (markerIndex < 0) { return null; } return ( normalizedId.slice(markerIndex + marker.length).split('/', 1)[0] ?? null ); } function isRetiredTopLevelSourceModule(normalizedId: string) { const marker = '/src/'; const markerIndex = normalizedId.lastIndexOf(marker); if (markerIndex < 0) { return false; } const relativePath = normalizedId.slice(markerIndex + marker.length); const segments = relativePath.split('/'); if (segments.length !== 2) { return false; } const [directory, fileName] = segments; const activeModules = directory ? ACTIVE_TOP_LEVEL_SOURCE_MODULES.get(directory) : undefined; return Boolean(activeModules && fileName && !activeModules.has(fileName)); } function readSourceDirectory(normalizedId: string) { const marker = '/src/'; const markerIndex = normalizedId.lastIndexOf(marker); if (markerIndex < 0) { return null; } return ( normalizedId.slice(markerIndex + marker.length).split('/', 1)[0] ?? null ); } export function isRetiredFrontendModuleId(id: string) { const normalizedId = normalizeViteModuleId(id); if (!normalizedId.startsWith(`${ROOT_SOURCE_DIRECTORY}/`)) { return false; } const sourceDirectory = readSourceDirectory(normalizedId); if ( isRetiredTopLevelSourceModule(normalizedId) || (sourceDirectory && RETIRED_SOURCE_MODULE_DIRECTORIES.has(sourceDirectory)) || normalizedId.includes('/src/types/') || RETIRED_STANDALONE_MODULE_PATHS.some((path) => normalizedId.endsWith(path), ) || RETIRED_STANDALONE_MODULE_PATTERNS.some((pattern) => pattern.test(normalizedId), ) ) { return true; } const componentDirectory = readTopLevelSourceDirectory( normalizedId, 'components', ); if ( componentDirectory && RETIRED_COMPONENT_MODULE_DIRECTORIES.has(componentDirectory) ) { return true; } const serviceDirectory = readTopLevelSourceDirectory( normalizedId, 'services', ); if ( serviceDirectory && RETIRED_SERVICE_MODULE_DIRECTORIES.has(serviceDirectory) ) { return true; } const hookDirectory = readTopLevelSourceDirectory(normalizedId, 'hooks'); if ( hookDirectory && RETIRED_HOOK_MODULE_DIRECTORIES.has(hookDirectory) ) { return true; } return RETIRED_PLATFORM_ENTRY_MODULE_PATTERN.test(normalizedId); } function retiredCreationTemplateModulesPlugin(): Plugin { return { name: 'retired-creation-template-modules', enforce: 'pre', transform(_code, id) { if (!isRetiredFrontendModuleId(id)) { return null; } throw new Error( `退役创作模板模块不得进入现役 Vite 依赖图:${normalizeViteModuleId(id)}`, ); }, }; } export function isForbiddenPublicAssetPath(pathname: string) { return ( pathname.startsWith('/generated-') || RETIRED_PUBLIC_ASSET_PATHS.some( (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), ) ); } export function isRetiredApiPath(pathname: string) { return RETIRED_API_PATHS.some( (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), ); } function retiredCreationTemplateAssetsPlugin(): Plugin { return { name: 'retired-creation-template-assets', configureServer(server) { server.middlewares.use((request, response, next) => { const pathname = new URL(request.url ?? '/', 'http://localhost') .pathname; if ( !isForbiddenPublicAssetPath(pathname) && !isRetiredApiPath(pathname) ) { next(); return; } response.statusCode = 404; response.end(); }); }, writeBundle(options) { const outputDir = path.resolve(__dirname, options.dir ?? 'dist'); for (const assetPath of RETIRED_PUBLIC_ASSET_PATHS) { rmSync(path.join(outputDir, assetPath.slice(1)), { force: true, recursive: true, }); } }, }; } function retiredCreationTemplateCssPlugin(): Plugin { const stripRetiredCss = (code: string, from: string) => { const root = postcss.parse(code, { from }); root.walkRules((rule) => { if (RETIRED_TEMPLATE_CSS_PATTERN.test(rule.selector)) { rule.remove(); } }); root.walkAtRules((atRule) => { if ( (atRule.name === 'keyframes' || atRule.name === 'source') && RETIRED_TEMPLATE_CSS_PATTERN.test(atRule.params) ) { atRule.remove(); } }); root.walkDecls((declaration) => { if ( RETIRED_TEMPLATE_CSS_PATTERN.test(declaration.prop) || RETIRED_TEMPLATE_CSS_PATTERN.test(declaration.value) ) { declaration.remove(); } }); return root.toString(); }; return { name: 'retired-creation-template-css', enforce: 'pre', transform(code, id) { if ( !id.endsWith('/src/index.css') && !id.endsWith('/packages/shared/src/theme.css') ) { return null; } const activeSourceCode = id.endsWith('/src/index.css') ? code.replace("@import 'tailwindcss';", ACTIVE_TAILWIND_SOURCES) : code; return { code: stripRetiredCss(activeSourceCode, id), map: null }; }, generateBundle(_options, bundle) { Object.values(bundle).forEach((entry) => { if (entry.type !== 'asset' || !entry.fileName.endsWith('.css')) { return; } entry.source = stripRetiredCss( typeof entry.source === 'string' ? entry.source : new TextDecoder().decode(entry.source), entry.fileName, ); }); }, }; } export default defineConfig(({ mode }) => { const env = loadEnv(mode, __dirname, ''); const ignoredWatchGlobs = [ // Prevent a root-level nohup log from triggering a Vite reload loop. '**/nohup.out', '**/.git/**', '**/.worktrees/**', '**/dist/**', '**/dist_check/**', '**/dist_check_final/**', '**/dist_check_monster_position/**', '**/temp*build*/**', '**/public/generated-character-drafts/**', '**/public/generated-characters/**', '**/public/generated-custom-world-scenes/**', '**/public/generated-qwen-sprites/**', '**/public/child-motion-demo/**', '**/public/anthro-cat-illustrations/**', '**/public/bark-battle-assets/**', '**/public/character/**', '**/public/creation-type-references/**', '**/public/edutainment-baby-object/**', '**/public/match3d-background-references/**', '**/public/match3d-style-references/**', '**/public/puzzle-creation-templates/**', '**/public/visual-novel-style-references/**', '**/public/wooden-fish/**', '**/src/components/child-motion-demo/**', '**/src/components/asset-studio/**', '**/src/components/bark-battle-creation/**', '**/src/components/big-fish-*/**', '**/src/components/creation-agent/**', '**/src/components/creative-agent/**', '**/src/components/custom-world-*/**', '**/src/components/edutainment-*/**', '**/src/ChildMotionDemoApp.tsx', '**/src/Match3DPlaygroundApp.tsx', '**/src/components/jump-hop-result/**', '**/src/components/jump-hop-runtime/**', '**/src/components/match3d-result/**', '**/src/components/match3d-runtime/**', '**/src/components/puzzle-*/**', '**/src/components/rpg-entry/**', '**/src/components/rpg-creation-*/**', '**/src/components/rpg-runtime-*/**', '**/src/components/square-hole-*/**', '**/src/components/unified-creation/**', '**/src/components/visual-novel-*/**', '**/src/components/wooden-fish-*/**', '**/src/components/platform-entry/platformMatch3DRuntimeProfile*', '**/src/components/unified-creation/workspaces/JumpHopCreationWorkspace*', '**/src/components/unified-creation/workspaces/Match3DCreationWorkspace*', '**/src/services/child-motion-demo/**', '**/src/services/bark-battle-*/**', '**/src/services/big-fish-*/**', '**/src/services/creation-*/**', '**/src/services/creative-agent/**', '**/src/services/edutainment-*/**', '**/src/services/jump-hop/**', '**/src/services/match3d-creation/**', '**/src/services/match3d-runtime/**', '**/src/services/match3d-works/**', '**/src/services/match3dGeneratedModelCache*', '**/src/services/match3dSpritesheetParser*', '**/src/services/puzzle-*/**', '**/src/services/rpg-entry/**', '**/src/services/rpg-creation/**', '**/src/services/rpg-runtime/**', '**/src/services/square-hole-*/**', '**/src/services/visual-novel-*/**', '**/src/services/wooden-fish/**', '**/backend-rewrite-tasklist/**', '**/docs/**', '**/jenkins/**', '**/media/**', '**/scripts/**', '**/server-rs/**', '**/server-rs/target/**', '**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', ]; const rustServerTarget = env.RUST_SERVER_TARGET || env.GENARRATIVE_API_TARGET || `http://127.0.0.1:${env.GENARRATIVE_API_PORT || '3100'}`; const runtimeServerTarget = env.GENARRATIVE_RUNTIME_SERVER_TARGET || rustServerTarget; const adminWebTarget = env.ADMIN_WEB_TARGET || `http://127.0.0.1:${env.ADMIN_WEB_PORT || '3102'}`; return { root: __dirname, envDir: __dirname, plugins: [ retiredCreationTemplateModulesPlugin(), retiredCreationTemplateCssPlugin(), react(), tailwindcss(), retiredCreationTemplateAssetsPlugin(), ], define: { 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), }, resolve: { alias: { '@': path.resolve(__dirname, '.'), }, }, optimizeDeps: { // Only crawl the real app entry so temporary build folders do not get // treated as extra HTML entrypoints during dependency scanning. entries: ['index.html'], }, build: { chunkSizeWarningLimit: 1000, rollupOptions: { output: { manualChunks(id) { if (id.includes('/node_modules/motion')) { return 'motion-vendor'; } if (id.includes('/src/services/')) { return 'app-services'; } }, }, }, }, server: { allowedHosts: ['outfield-outlook-reprocess.ngrok-free.dev'], // HMR is disabled in AI Studio via DISABLE_HMR env var. // Do not modify; file watching is disabled to prevent flickering during agent edits. hmr: process.env.DISABLE_HMR !== 'true', proxy: { '/admin/': { target: adminWebTarget, changeOrigin: true, secure: false, }, '/api/auth': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/profile': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/runtime': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/editor': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/assets': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/llm': { target: runtimeServerTarget, changeOrigin: true, secure: false, }, '/api/ws': { target: runtimeServerTarget, changeOrigin: true, secure: false, ws: true, }, }, watch: { ignored: ignoredWatchGlobs, }, }, }; });