refactor: dynamic routes

This commit is contained in:
jialin
2026-05-08 09:33:16 +08:00
committed by jialin
parent 4be5fb4d3d
commit f08d5c904c
8 changed files with 8 additions and 309 deletions
+3
View File
@@ -0,0 +1,3 @@
// Identity hook for build-time route extensions. Tooling may overwrite
// this file to inject additional routes; the original is restored on cleanup.
export const applyRouteExtensions = <T>(base: T): T => base;
+4 -11
View File
@@ -1,6 +1,7 @@
import { keepAliveRoutes } from './keep-alive';
import { applyRouteExtensions } from './routes.extensions';
export default [
const baseRoutes = [
{
name: 'dashboard',
path: '/dashboard',
@@ -312,16 +313,6 @@ export default [
defaultIcon: 'icon-key',
component: './api-keys'
},
{
name: 'settings',
path: '/settings',
key: 'settings',
hideInMenu: true,
selectedIcon: 'icon-settings-filled',
icon: 'icon-settings',
defaultIcon: 'icon-settings',
component: './settings'
},
{
name: 'profile',
path: '/profile',
@@ -346,3 +337,5 @@ export default [
component: './404'
}
];
export default applyRouteExtensions(baseRoutes);
+1 -7
View File
@@ -3,20 +3,14 @@
"author": "gpustack",
"scripts": {
"build": "max build",
"build:enterprise": "node ./scripts/run-enterprise-build.cjs",
"check:locales": "node --import tsx ./src/locales/check.ts",
"dev": "max dev",
"dev:enterprise": "node ./scripts/run-enterprise-dev.cjs",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"prepare": "husky",
"preview": "max preview",
"setup": "max setup",
"start": "npm run dev",
"sync:enterprise-config": "node ./scripts/sync-enterprise-config.cjs",
"sync:enterprise-config:clean": "node ./scripts/sync-enterprise-config.cjs clean",
"sync:enterprise-global": "npm run sync:enterprise-config",
"sync:enterprise-global:clean": "npm run sync:enterprise-config:clean"
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
-47
View File
@@ -1,47 +0,0 @@
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const syncScript = path.resolve(__dirname, './sync-enterprise-config.cjs');
const runNodeScript = (args) => {
const result = spawnSync(process.execPath, [syncScript, ...args], {
stdio: 'inherit'
});
if (result.error) {
throw result.error;
}
if (typeof result.status === 'number' && result.status !== 0) {
process.exit(result.status);
}
};
const runBuild = () =>
spawnSync('max', ['build'], {
env: {
...process.env,
ENABLE_ENTERPRISE: 'true'
},
shell: true,
stdio: 'inherit'
});
let buildResult;
try {
runNodeScript([]);
buildResult = runBuild();
} finally {
try {
runNodeScript(['clean']);
} catch (error) {
console.error('Failed to clean enterprise config:', error);
}
}
if (buildResult?.error) {
throw buildResult.error;
}
process.exit(buildResult?.status ?? 0);
-69
View File
@@ -1,69 +0,0 @@
const { spawn, spawnSync } = require('node:child_process');
const path = require('node:path');
const syncScript = path.resolve(__dirname, './sync-enterprise-config.cjs');
const runNodeScript = (args) => {
const result = spawnSync(process.execPath, [syncScript, ...args], {
stdio: 'inherit'
});
if (typeof result.status === 'number' && result.status !== 0) {
process.exit(result.status);
}
if (result.error) {
throw result.error;
}
};
runNodeScript([]);
const child = spawn('max', ['dev'], {
env: {
...process.env,
ENABLE_ENTERPRISE: 'true'
},
shell: true,
stdio: 'inherit'
});
let cleaned = false;
const clean = () => {
if (cleaned) {
return;
}
cleaned = true;
try {
runNodeScript(['clean']);
} catch (error) {
console.error('Failed to clean enterprise config:', error);
}
};
const forwardSignal = (signal) => {
if (!child.killed) {
child.kill(signal);
}
};
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => {
process.on(signal, () => {
forwardSignal(signal);
});
});
child.on('error', (error) => {
clean();
throw error;
});
child.on('exit', (code, signal) => {
clean();
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
-96
View File
@@ -1,96 +0,0 @@
const fs = require('node:fs');
const path = require('node:path');
const workspaceGlobalPath = path.resolve(__dirname, '../../../global.tsx');
const workspaceDependenciesPath = path.resolve(__dirname, '../../../dependencies.json');
const workspaceTsconfigPath = path.resolve(__dirname, '../../../tsconfig.json');
const targetGlobalPath = path.resolve(__dirname, '../src/global.tsx');
const targetPackageJsonPath = path.resolve(__dirname, '../package.json');
const targetTsconfigPath = path.resolve(__dirname, '../tsconfig.json');
const mode = process.argv[2] || 'append';
const blockStart = '// ENTERPRISE_PLUGIN_BLOCK_START';
const blockEnd = '// ENTERPRISE_PLUGIN_BLOCK_END';
const workspaceSource = fs.readFileSync(workspaceGlobalPath, 'utf8').trim();
const blockContent = `\n${blockStart}\n${workspaceSource}\n${blockEnd}\n`;
const stripBlock = (content) => {
const pattern = new RegExp(`\\n?${blockStart}[\\s\\S]*?${blockEnd}\\n?`, 'g');
return content.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
};
const currentContent = fs.readFileSync(targetGlobalPath, 'utf8');
const strippedContent = stripBlock(currentContent);
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
const writeJson = (filePath, data) => {
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
};
const syncPackageJson = () => {
const sourceDependencies =
readJson(workspaceDependenciesPath).dependencies || {};
const targetPackageJson = readJson(targetPackageJsonPath);
const currentDependencies = {
...(targetPackageJson.dependencies || {})
};
if (mode === 'clean') {
Object.keys(sourceDependencies).forEach((dependencyName) => {
delete currentDependencies[dependencyName];
});
} else {
Object.assign(currentDependencies, sourceDependencies);
}
targetPackageJson.dependencies = currentDependencies;
writeJson(targetPackageJsonPath, targetPackageJson);
console.log(
`${mode === 'clean' ? 'Cleaned' : 'Synced'} enterprise dependencies in ${targetPackageJsonPath}`,
);
};
const syncTsconfig = () => {
const sourcePaths =
readJson(workspaceTsconfigPath).compilerOptions?.paths || {};
const targetTsconfig = readJson(targetTsconfigPath);
const currentPaths = {
...(targetTsconfig.compilerOptions?.paths || {})
};
if (mode === 'clean') {
Object.keys(sourcePaths).forEach((pathKey) => {
delete currentPaths[pathKey];
});
} else {
Object.assign(currentPaths, sourcePaths);
}
targetTsconfig.compilerOptions = targetTsconfig.compilerOptions || {};
targetTsconfig.compilerOptions.paths = currentPaths;
writeJson(targetTsconfigPath, targetTsconfig);
console.log(
`${mode === 'clean' ? 'Cleaned' : 'Synced'} enterprise tsconfig paths in ${targetTsconfigPath}`,
);
};
if (mode === 'clean') {
if (strippedContent !== currentContent) {
fs.writeFileSync(targetGlobalPath, strippedContent);
console.log(`Cleaned enterprise plugin block from ${targetGlobalPath}`);
} else {
console.log(`Enterprise plugin block already absent: ${targetGlobalPath}`);
}
} else {
const updatedContent = `${strippedContent.trimEnd()}${blockContent}`;
if (updatedContent !== currentContent) {
fs.writeFileSync(targetGlobalPath, updatedContent);
console.log(`Appended enterprise plugin block to ${targetGlobalPath}`);
} else {
console.log(`Enterprise plugin block already up to date: ${targetGlobalPath}`);
}
}
syncPackageJson();
syncTsconfig();
-22
View File
@@ -1,22 +0,0 @@
import { getGPUStackPlugin } from '@/plugins';
import { Result } from 'antd';
import React from 'react';
const SettingsPage: React.FC = () => {
const enterprisePlugin = getGPUStackPlugin();
const BrandingUI = enterprisePlugin?.components?.BrandingUI;
if (!BrandingUI) {
return (
<Result
status="404"
title="404"
subTitle="Enterprise settings page is unavailable."
/>
);
}
return <BrandingUI />;
};
export default SettingsPage;
-57
View File
@@ -1,57 +0,0 @@
import { getGPUStackPlugin } from '@/plugins';
import type { RouteConfig } from '@/plugins/types';
type ClientRoute = RouteConfig & {
id?: string;
children?: ClientRoute[];
routes?: ClientRoute[];
};
function getLayoutChildren(routes: ClientRoute[]): ClientRoute[] | null {
const rootRoute = routes.find((route) => route.path === '/');
if (!rootRoute) return null;
const rootChildren = rootRoute.routes || rootRoute.children;
if (!Array.isArray(rootChildren)) return null;
const globalLayout = rootChildren.find((route) =>
route.id?.includes('global-layout')
);
if (!globalLayout) return null;
const layoutChildren = globalLayout.children || globalLayout.routes;
return Array.isArray(layoutChildren) ? layoutChildren : null;
}
function mergeRoute(targetRoutes: ClientRoute[], pluginRoute: ClientRoute) {
const existsIndex = targetRoutes.findIndex(
(route) =>
(pluginRoute.path && route.path === pluginRoute.path) ||
(pluginRoute.key && route.key === pluginRoute.key)
);
if (existsIndex === -1) {
targetRoutes.push(pluginRoute);
return;
}
const currentRoute = targetRoutes[existsIndex];
targetRoutes[existsIndex] = {
...currentRoute,
...pluginRoute,
routes: pluginRoute.routes || currentRoute.routes,
children: pluginRoute.children || currentRoute.children
};
}
export function mergeEnterpriseRoutes(routes: ClientRoute[]) {
const enterprisePlugin = getGPUStackPlugin();
if (!enterprisePlugin?.routes?.length) return;
const layoutChildren = getLayoutChildren(routes);
if (!layoutChildren) return;
enterprisePlugin.routes.forEach((route) =>
mergeRoute(layoutChildren, route as ClientRoute)
);
}