Files
Genarrative/vite.config.ts
T
kdletters c5200c7d45
Project CI / Backend tests (push) Successful in 8m31s
Project CI / Repository checks (push) Successful in 14m14s
Project CI / Frontend tests (push) Successful in 13m2s
Project CI / Native shell tests (push) Successful in 21m30s
补齐共享 UI 组件库与展示页交互 (#202)
## 变更内容

- 新增基于 shadcn open-code 模式的共享 UI canonical 组件、样式与导出。
- 新增 `/components` 共享组件展示页,并覆盖平台组件、Token、状态和移动端布局。
- 补齐平台展示页的筛选、排序、上传预览、标签、开关和异步状态交互。
- 修复排序导致预览主题变化、筛选弹窗误关闭和入口状态不更新的问题。
- 同步网站与客户端构建别名、依赖、路由测试和项目文档。

## 验证

- `npm run test -- src/components/shared-components/SharedComponentsShowcasePage.test.tsx`
- `npm run typecheck`
- `npm run check:encoding`
- `npm run build:raw`
- `git diff --check`
- pre-push 门禁通过

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/202
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-08-31 14:10:44 +08:00

711 lines
21 KiB
TypeScript

import { rmSync } from 'node:fs';
import { createRequire } from 'node:module';
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 aiGameCreatorRequire = createRequire(
path.resolve(__dirname, 'apps/ai-game-creator-shell/package.json'),
);
const reactFileManagerPackageRoot = path.dirname(
aiGameCreatorRequire.resolve('@cubone/react-file-manager/package.json'),
);
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<string, ReadonlySet<string>>([
[
'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',
'/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: [
{
find: /^@cubone\/react-file-manager$/,
replacement: path.resolve(
reactFileManagerPackageRoot,
'dist/react-file-manager.es.js',
),
},
{ find: '@', replacement: path.resolve(__dirname, '.') },
{
find: '@genarrative/image-canvas-core',
replacement: path.resolve(
__dirname,
'packages/image-canvas-core/src/index.ts',
),
},
{
find: '@genarrative/image-canvas-react/styles.css',
replacement: path.resolve(
__dirname,
'packages/image-canvas-react/src/styles.css',
),
},
{
find: '@genarrative/image-canvas-react',
replacement: path.resolve(
__dirname,
'packages/image-canvas-react/src/index.ts',
),
},
{
find: '@genarrative/shared/styles.css',
replacement: path.resolve(
__dirname,
'packages/shared/src/components/styles.css',
),
},
{
find: '@genarrative/shared/theme.css',
replacement: path.resolve(__dirname, 'packages/shared/src/theme.css'),
},
{
find: '@genarrative/shared/components/ui',
replacement: path.resolve(
__dirname,
'packages/shared/src/components/ui/index.ts',
),
},
{
find: '@genarrative/shared/lib',
replacement: path.resolve(__dirname, 'packages/shared/src/lib'),
},
{
find: '@genarrative/shared/components/account',
replacement: path.resolve(
__dirname,
'packages/shared/src/components/account.ts',
),
},
{
find: '@genarrative/shared/components',
replacement: path.resolve(
__dirname,
'packages/shared/src/components/index.ts',
),
},
{
find: '@genarrative/shared',
replacement: path.resolve(__dirname, 'packages/shared/src/index.ts'),
},
],
dedupe: ['react', 'react-dom'],
},
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,
},
},
};
});