import { readdir } from 'node:fs/promises'; import path from 'node:path'; import { ESLint } from 'eslint'; import type { Plugin } from 'vite'; import { describe, expect, it } from 'vitest'; import viteConfig, { isForbiddenPublicAssetPath, isRetiredApiPath, isRetiredFrontendModuleId, } from '../vite.config'; async function resolveVitePlugin(name: string) { const resolvedConfig = typeof viteConfig === 'function' ? await viteConfig({ command: 'serve', mode: 'test' }) : viteConfig; const plugins = (resolvedConfig.plugins ?? []).flat(Infinity) as Plugin[]; return plugins.find((plugin) => plugin?.name === name); } function resolveRetiredCssPlugin() { return resolveVitePlugin('retired-creation-template-css'); } function workspacePath(relativePath: string) { return path.resolve(process.cwd(), relativePath).replaceAll('\\', '/'); } async function readTypeScriptSourceFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); const files = await Promise.all( entries.map(async (entry) => { const entryPath = path.join(directory, entry.name); if (entry.isDirectory()) { return readTypeScriptSourceFiles(entryPath); } if ( !entry.isFile() || !/\.(?:ts|tsx)$/u.test(entry.name) || entry.name.endsWith('.d.ts') ) { return []; } return [entryPath.replaceAll('\\', '/')]; }), ); return files.flat(); } describe('retired creation template CSS plugin', () => { it('runs before Tailwind turns source CSS into a Vite JavaScript module', async () => { const plugin = await resolveRetiredCssPlugin(); expect(plugin).toBeDefined(); expect(plugin?.enforce).toBe('pre'); }); it('keeps the active creation landing CSS while removing retired template CSS', async () => { const plugin = await resolveRetiredCssPlugin(); const transform = plugin?.transform; expect(typeof transform).toBe('function'); if (typeof transform !== 'function') { return; } const result = await transform.call( {} as never, "@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }\n.pixel-modal-shell { color: black; }\n@font-face { font-family: 'Fusion Pixel'; src: url('/fusion-pixel.ttf'); }", '/workspace/src/index.css', ); const code = typeof result === 'string' ? result : result?.code; expect(code).toContain('.creation-landing'); expect(code).not.toContain('.puzzle-runtime'); expect(code).not.toContain('.pixel-modal-shell'); expect(code).not.toContain('fusion-pixel.ttf'); expect(code).toContain('@source "./components/creation-home"'); }); }); describe('retired creation template module boundary plugin', () => { it('rejects retired component and service modules from the active Vite graph', async () => { const plugin = await resolveVitePlugin('retired-creation-template-modules'); const transform = plugin?.transform; expect(plugin?.enforce).toBe('pre'); expect(typeof transform).toBe('function'); if (typeof transform !== 'function') { return; } expect(() => transform.call( {} as never, 'export {}', workspacePath('src/components/rpg-entry/RpgEntryHomeView.tsx'), ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); expect(() => transform.call( {} as never, 'export {}', workspacePath('src/services/runtimeRequest.ts'), ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); expect(() => transform.call( {} as never, 'export {}', workspacePath( 'src/services/input-devices/runtimeDragInputController.ts', ), ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); expect(() => transform.call( {} as never, 'export {}', workspacePath('src/services/runtimeAudioFeedback.ts'), ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); expect(() => transform.call( {} as never, 'export {}', workspacePath('src/types/game.ts'), ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); expect(() => transform.call( {} as never, 'export {}', `${workspacePath('src/services/rpg-entry/rpgProfileClient.ts')}?t=1`, ), ).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u); }); it('rejects retired top-level apps, data, games, prompts, routes, and services', () => { for (const path of [ 'src/App.test.tsx', 'src/App.tsx', 'src/RpgRuntimeApp.tsx', 'src/Match3DPlaygroundApp.tsx', 'src/components/CustomWorldGenerationView.tsx', 'src/components/InventoryPanel.tsx', 'src/components/common/PublishShareModal.tsx', 'src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx', 'src/components/platform-entry/platformMatch3DRuntimeProfile.ts', 'src/data/customWorldLibrary.ts', 'src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx', 'src/hooks/combat/battlePlan.ts', 'src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts', 'src/hooks/rpg-session/useRpgRuntimeSession.ts', 'src/hooks/useNpcInteractionFlow.ts', 'src/persistence/runtimeSnapshot.ts', 'src/prompts/customWorldPrompts.ts', 'src/routing/appRoutes.tsx', 'src/routing/runtimeNotFoundRecovery.ts', 'src/services/ai.ts', 'src/services/puzzleReferenceImage.ts', ]) { expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(true); } }); it('allows active creation, project, and profile modules', async () => { const plugin = await resolveVitePlugin('retired-creation-template-modules'); const transform = plugin?.transform; expect(typeof transform).toBe('function'); if (typeof transform !== 'function') { return; } expect( transform.call( {} as never, 'export {}', workspacePath('src/components/creation-home/CreationLandingView.tsx'), ), ).toBeNull(); expect( transform.call( {} as never, 'export {}', workspacePath( 'src/components/platform-entry/PlatformActiveProfileView.tsx', ), ), ).toBeNull(); expect( transform.call( {} as never, 'export {}', workspacePath('src/services/platform-entry/platformProfileClient.ts'), ), ).toBeNull(); expect(isRetiredFrontendModuleId(workspacePath('src/ActiveApp.tsx'))).toBe( false, ); expect( isRetiredFrontendModuleId( workspacePath('src/services/image-editor/editorProjectClient.ts'), ), ).toBe(false); expect( isRetiredFrontendModuleId( workspacePath('src/components/ResolvedAssetAudio.tsx'), ), ).toBe(false); expect( isRetiredFrontendModuleId( workspacePath('src/components/ResolvedAssetImage.tsx'), ), ).toBe(false); for (const path of [ 'src/hooks/useGameSettings.ts', 'src/persistence/storage.ts', 'src/routing/activeAppRoutes.tsx', 'src/services/apiClient.ts', ]) { expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(false); } expect( isRetiredFrontendModuleId( workspacePath( 'packages/shared/src/components/PlatformMudPointWalletEntry.tsx', ), ), ).toBe(false); }); }); describe('retired frontend ESLint boundary', () => { it('keeps every ESLint-excluded source module outside the Vite graph', async () => { const eslint = new ESLint({ cwd: process.cwd() }); const sourceFiles = await readTypeScriptSourceFiles('src'); const retiredSourceFiles: string[] = []; for (const sourceFile of sourceFiles) { if (await eslint.isPathIgnored(sourceFile)) { retiredSourceFiles.push(sourceFile); } } expect(retiredSourceFiles.length).toBeGreaterThan(0); expect( retiredSourceFiles.filter( (sourceFile) => !isRetiredFrontendModuleId(workspacePath(sourceFile)), ), ).toEqual([]); }); it('ignores retired root modules while keeping active root modules lintable', async () => { const eslint = new ESLint({ cwd: process.cwd() }); for (const path of [ 'src/components/CustomWorldGenerationView.tsx', 'src/hooks/useNpcInteractionFlow.ts', 'src/persistence/runtimeSnapshot.ts', 'src/routing/runtimeNotFoundRecovery.ts', 'src/services/puzzleReferenceImage.ts', ]) { await expect(eslint.isPathIgnored(path)).resolves.toBe(true); } for (const path of [ 'src/components/ResolvedAssetAudio.tsx', 'src/components/ResolvedAssetImage.tsx', 'src/hooks/useGameSettings.ts', 'src/persistence/storage.ts', 'src/routing/activeAppRoutes.tsx', 'src/services/apiClient.ts', ]) { await expect(eslint.isPathIgnored(path)).resolves.toBe(false); } }); }); describe('retired creation template API boundary', () => { it('returns a dev-server 404 boundary for retired API prefixes only', () => { expect(isRetiredApiPath('/api/creation-entry/config')).toBe(true); expect(isRetiredApiPath('/api/creation/puzzle/sessions')).toBe(true); expect(isRetiredApiPath('/api/public-works/PZ-12345678')).toBe(true); expect(isRetiredApiPath('/api/runtime/settings')).toBe(false); expect(isRetiredApiPath('/api/editor/showcase/resources')).toBe(false); }); }); describe('retired creation template asset boundary', () => { it('returns a dev-server 404 boundary for retired generated asset proxies', () => { for (const pathname of [ '/generated-character-drafts/hero/visual/candidate.png', '/generated-characters/hero/visual/master.png', '/generated-animations/hero/idle/frame01.png', '/generated-big-fish-assets/session-1/level/image.png', '/generated-puzzle-assets/session-1/candidate/image.png', '/generated-custom-world-scenes/world-1/camp/scene.png', '/generated-custom-world-covers/world-1/cover.webp', '/generated-bark-battle-assets/draft/player/image.webp', '/generated-qwen-sprites/master/candidate-01.png', ]) { expect(isForbiddenPublicAssetPath(pathname)).toBe(true); } expect( isForbiddenPublicAssetPath('/generated-editor-images/asset.png'), ).toBe(true); expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false); expect( isForbiddenPublicAssetPath( '/branding/mobile-home-welcome-taonier-ip.png', ), ).toBe(false); }); });