Files
Genarrative/scripts/vite-retired-css-plugin.test.ts
T
kdletters ee3948b491 合并旧创作模板退役分支
合并 codex/retire-legacy-creation,退役旧创作模板业务并保留历史数据壳
保留 master 最新编辑器 Agent、画布和个人页能力
补齐现役编辑器 Agent 的 LLM 与生成结果读取链路
同步 Vite、ESLint、Rust workspace、SpacetimeDB 与文档边界
2026-07-20 20:06:26 +08:00

316 lines
10 KiB
TypeScript

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<string[]> {
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);
});
});