feat: introduce runtime path (#132)

This commit is contained in:
samanhappy
2025-05-27 18:34:23 +08:00
committed by GitHub
parent 268ce5cce6
commit a1047321d1
17 changed files with 226 additions and 46 deletions

View File

@@ -12,16 +12,7 @@ import GroupsPage from './pages/GroupsPage';
import SettingsPage from './pages/SettingsPage';
import MarketPage from './pages/MarketPage';
import LogsPage from './pages/LogsPage';
// Get base path from environment variable or default to empty string
const getBasePath = (): string => {
const basePath = import.meta.env.BASE_PATH || '';
// Ensure the path starts with / if it's not empty and doesn't already start with /
if (basePath && !basePath.startsWith('/')) {
return '/' + basePath;
}
return basePath;
};
import { getBasePath } from './utils/runtime';
function App() {
const basename = getBasePath();

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ServerForm from './ServerForm'
import { getApiUrl } from '../utils/api'
import { getApiUrl } from '../utils/runtime';
interface AddServerFormProps {
onAdd: () => void

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Server } from '@/types'
import { getApiUrl } from '../utils/runtime'
import ServerForm from './ServerForm'
interface EditServerFormProps {
@@ -17,7 +18,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
try {
setError(null)
const token = localStorage.getItem('mcphub_token');
const response = await fetch(`/api/servers/${server.name}`, {
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Group, ApiResponse } from '@/types';
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
export const useGroupData = () => {
const { t } = useTranslation();

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { MarketServer, ApiResponse } from '@/types';
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
export const useMarketData = () => {
const { t } = useTranslation();

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Server, ApiResponse } from '@/types';
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
// Configuration options
const CONFIG = {
@@ -204,7 +204,7 @@ export const useServerData = () => {
try {
// Fetch settings to get the full server config before editing
const token = localStorage.getItem('mcphub_token');
const response = await fetch(`/api/settings`, {
const response = await fetch(getApiUrl('/settings'), {
headers: {
'x-auth-token': token || '',
},
@@ -241,7 +241,7 @@ export const useServerData = () => {
const handleServerRemove = async (serverName: string) => {
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch(`/api/servers/${serverName}`, {
const response = await fetch(getApiUrl(`/servers/${serverName}`), {
method: 'DELETE',
headers: {
'x-auth-token': token || '',
@@ -265,7 +265,7 @@ export const useServerData = () => {
const handleServerToggle = async (server: Server, enabled: boolean) => {
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch(`/api/servers/${server.name}/toggle`, {
const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ApiResponse } from '@/types';
import { useToast } from '@/contexts/ToastContext';
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
// Define types for the settings data
interface RoutingConfig {

View File

@@ -1,12 +1,45 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
// Import the i18n configuration
import './i18n'
import './i18n';
import { loadRuntimeConfig } from './utils/runtime';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
// Load runtime configuration before starting the app
async function initializeApp() {
try {
console.log('Loading runtime configuration...');
const config = await loadRuntimeConfig();
console.log('Runtime configuration loaded:', config);
// Store config in window object
window.__MCPHUB_CONFIG__ = config;
// Start React app
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
} catch (error) {
console.error('Failed to initialize app:', error);
// Fallback: start app with default config
console.log('Starting app with default configuration...');
window.__MCPHUB_CONFIG__ = {
basePath: '',
version: 'dev',
name: 'mcphub',
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}
}
// Initialize the app
initializeApp();

View File

@@ -4,7 +4,7 @@ import {
RegisterCredentials,
ChangePasswordCredentials,
} from '../types';
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
// Token key in localStorage
const TOKEN_KEY = 'mcphub_token';

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { getToken } from './authService'; // Import getToken function
import { getApiUrl } from '../utils/api';
import { getApiUrl } from '../utils/runtime';
export interface LogEntry {
timestamp: number;

View File

@@ -0,0 +1,15 @@
// Global runtime configuration interface
export interface RuntimeConfig {
basePath: string;
version: string;
name: string;
}
// Extend Window interface to include runtime config
declare global {
interface Window {
__MCPHUB_CONFIG__?: RuntimeConfig;
}
}
export {};

View File

@@ -1,27 +1,28 @@
/**
* API utility functions for constructing URLs with proper base path support
*
* @deprecated Use functions from utils/runtime.ts instead for runtime configuration support
*/
import { getApiBaseUrl as getRuntimeApiBaseUrl, getApiUrl as getRuntimeApiUrl } from './runtime';
/**
* Get the API base URL including base path and /api prefix
* @returns The complete API base URL
* @deprecated Use getApiBaseUrl from utils/runtime.ts instead
*/
export const getApiBaseUrl = (): string => {
const basePath = import.meta.env.BASE_PATH || '';
// Ensure the path starts with / if it's not empty and doesn't already start with /
const normalizedBasePath = basePath && !basePath.startsWith('/') ? '/' + basePath : basePath;
// Always append /api to the base path for API endpoints
return normalizedBasePath + '/api';
console.warn('getApiBaseUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
return getRuntimeApiBaseUrl();
};
/**
* Construct a full API URL with the given endpoint
* @param endpoint - The API endpoint (should start with /, e.g., '/auth/login')
* @returns The complete API URL
* @deprecated Use getApiUrl from utils/runtime.ts instead
*/
export const getApiUrl = (endpoint: string): string => {
const baseUrl = getApiBaseUrl();
// Ensure endpoint starts with /
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
return baseUrl + normalizedEndpoint;
console.warn('getApiUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
return getRuntimeApiUrl(endpoint);
};

View File

@@ -0,0 +1,105 @@
import type { RuntimeConfig } from '../types/runtime';
/**
* Get runtime configuration from window object
*/
export const getRuntimeConfig = (): RuntimeConfig => {
return (
window.__MCPHUB_CONFIG__ || {
basePath: '',
version: 'dev',
name: 'mcphub',
}
);
};
/**
* Get the base path from runtime configuration
*/
export const getBasePath = (): string => {
const config = getRuntimeConfig();
const basePath = config.basePath || '';
// Ensure the path starts with / if it's not empty and doesn't already start with /
if (basePath && !basePath.startsWith('/')) {
return '/' + basePath;
}
return basePath;
};
/**
* Get the API base URL including base path and /api prefix
*/
export const getApiBaseUrl = (): string => {
const basePath = getBasePath();
// Always append /api to the base path for API endpoints
return basePath + '/api';
};
/**
* Construct a full API URL with the given endpoint
*/
export const getApiUrl = (endpoint: string): string => {
const baseUrl = getApiBaseUrl();
// Ensure endpoint starts with /
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
return baseUrl + normalizedEndpoint;
};
/**
* Load runtime configuration from server
*/
export const loadRuntimeConfig = async (): Promise<RuntimeConfig> => {
try {
// For initial config load, we need to determine the correct path
// Try different possible paths based on current location
const currentPath = window.location.pathname;
const possibleConfigPaths = [
// If we're already on a subpath, try to use it
currentPath.replace(/\/[^\/]*$/, '') + '/config',
// Try root config
'/config',
// Try with potential base paths
...(currentPath.includes('/')
? [currentPath.split('/')[1] ? `/${currentPath.split('/')[1]}/config` : '/config']
: ['/config']),
];
for (const configPath of possibleConfigPaths) {
try {
const response = await fetch(configPath, {
method: 'GET',
headers: {
Accept: 'application/json',
'Cache-Control': 'no-cache',
},
});
if (response.ok) {
const data = await response.json();
if (data.success && data.data) {
return data.data;
}
}
} catch (error) {
// Continue to next path
console.debug(`Failed to load config from ${configPath}:`, error);
}
}
// Fallback to default config
console.warn('Could not load runtime config from server, using defaults');
return {
basePath: '',
version: 'dev',
name: 'mcphub',
};
} catch (error) {
console.error('Error loading runtime config:', error);
return {
basePath: '',
version: 'dev',
name: 'mcphub',
};
}
};

View File

@@ -3,7 +3,6 @@
interface ImportMeta {
readonly env: {
readonly PACKAGE_VERSION: string;
readonly BASE_PATH?: string; // Add base path environment variable
// Add other custom env variables here if needed
[key: string]: any;
};

View File

@@ -8,12 +8,13 @@ import { readFileSync } from 'fs';
// Get package.json version
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
// Get base path from environment variable
const basePath = process.env.BASE_PATH || '';
// For runtime configuration, we'll always use relative paths
// BASE_PATH will be determined at runtime
const basePath = '';
// https://vitejs.dev/config/
export default defineConfig({
base: basePath || './', // Use base path or relative paths for assets
base: './', // Always use relative paths for runtime configuration
plugins: [react(), tailwindcss()],
resolve: {
alias: {
@@ -21,9 +22,9 @@ export default defineConfig({
},
},
define: {
// Make package version and base path available as global variables
// Make package version available as global variable
// BASE_PATH will be loaded at runtime
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
'import.meta.env.BASE_PATH': JSON.stringify(basePath),
},
build: {
sourcemap: true, // Enable source maps for production build

View File

@@ -0,0 +1,30 @@
import { Request, Response } from 'express';
import config from '../config/index.js';
/**
* Get runtime configuration for frontend
*/
export const getRuntimeConfig = (req: Request, res: Response): void => {
try {
const runtimeConfig = {
basePath: config.basePath,
version: config.mcpHubVersion,
name: config.mcpHubName,
};
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.json({
success: true,
data: runtimeConfig,
});
} catch (error) {
console.error('Error getting runtime config:', error);
res.status(500).json({
success: false,
message: 'Failed to get runtime configuration',
});
}
};

View File

@@ -32,6 +32,7 @@ import {
} from '../controllers/marketController.js';
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
import { getRuntimeConfig } from '../controllers/configController.js';
import { auth } from '../middlewares/auth.js';
const router = express.Router();
@@ -104,6 +105,9 @@ export const initRoutes = (app: express.Application): void => {
changePassword,
);
// Runtime configuration endpoint (no auth required for frontend initialization)
app.get(`${config.basePath}/config`, getRuntimeConfig);
app.use(`${config.basePath}/api`, router);
};