mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
fix(frontend): include frontend/dist in npm package and support static file serving
- Add ccw/frontend/dist/ to package.json files field - Modify react-frontend.ts to detect and use production build - Add static file serving to server.ts with MIME type support - Update prepublishOnly to build frontend before publishing - Fix unused import in TerminalDashboardPage.tsx This fixes the 'Could not find React frontend directory' error when users install from npm.
This commit is contained in:
@@ -28,7 +28,6 @@ import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPan
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useQueueSchedulerStore } from '@/stores/queueSchedulerStore';
|
||||
|
||||
// ========== Main Page Component ==========
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join, extname } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
// Import route handlers
|
||||
import { handleStatusRoutes } from './routes/status-routes.js';
|
||||
import { handleCliRoutes, cleanupStaleExecutions } from './routes/cli-routes.js';
|
||||
@@ -53,6 +56,7 @@ import { getCorsOrigin } from './cors.js';
|
||||
import { csrfValidation } from './auth/csrf-middleware.js';
|
||||
import { getCsrfTokenManager } from './auth/csrf-manager.js';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { getFrontendStaticDir } from '../utils/react-frontend.js';
|
||||
|
||||
// Import health check service
|
||||
import { getHealthCheckService } from './services/health-check-service.js';
|
||||
@@ -77,6 +81,54 @@ interface ServerOptions {
|
||||
|
||||
type PostHandler = PostRequestHandler;
|
||||
|
||||
/**
|
||||
* MIME type mapping for static files
|
||||
*/
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
};
|
||||
|
||||
/**
|
||||
* Serve static file from frontend dist directory
|
||||
*/
|
||||
async function serveStaticFile(
|
||||
filePath: string,
|
||||
res: http.ServerResponse
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = await readFile(filePath);
|
||||
const ext = extname(filePath);
|
||||
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': mimeType,
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
});
|
||||
res.end(content);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Static] Error serving ${filePath}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST request with JSON body
|
||||
*/
|
||||
@@ -664,8 +716,31 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
return;
|
||||
}
|
||||
|
||||
// React frontend proxy - forward all non-API requests to Vite dev server
|
||||
// React frontend proxy - forward all non-API requests to Vite dev server or serve static files
|
||||
{
|
||||
const frontendStaticDir = getFrontendStaticDir();
|
||||
|
||||
// If we have a static build, serve files directly
|
||||
if (frontendStaticDir) {
|
||||
let filePath = join(frontendStaticDir, pathname === '/' ? 'index.html' : pathname.slice(1));
|
||||
|
||||
// Try to serve the file
|
||||
const served = await serveStaticFile(filePath, res);
|
||||
|
||||
// If file not found, serve index.html for SPA routing
|
||||
if (!served && !pathname.startsWith('/api/')) {
|
||||
filePath = join(frontendStaticDir, 'index.html');
|
||||
const indexServed = await serveStaticFile(filePath, res);
|
||||
|
||||
if (!indexServed) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Frontend not found');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, proxy to Vite dev server
|
||||
const reactUrl = `http://localhost:${reactPort}${pathname}${url.search}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,14 @@ const __dirname = dirname(__filename);
|
||||
|
||||
let reactProcess: ChildProcess | null = null;
|
||||
let reactPort: number | null = null;
|
||||
let frontendStaticDir: string | null = null;
|
||||
|
||||
/**
|
||||
* Get the frontend directory path (for serving static files in production)
|
||||
*/
|
||||
export function getFrontendStaticDir(): string | null {
|
||||
return frontendStaticDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start React frontend development server
|
||||
@@ -24,18 +32,29 @@ export async function startReactFrontend(port: number): Promise<void> {
|
||||
|
||||
// Try to find frontend directory (relative to ccw package)
|
||||
const possiblePaths = [
|
||||
join(__dirname, '../../frontend'), // From dist/utils
|
||||
join(__dirname, '../../frontend'), // From dist/utils (dev mode with full frontend)
|
||||
join(__dirname, '../frontend'), // From src/utils (dev)
|
||||
join(process.cwd(), 'frontend'), // Current working directory
|
||||
];
|
||||
|
||||
// Also check for production build (frontend/dist)
|
||||
const possibleDistPaths = [
|
||||
join(__dirname, '../../frontend/dist'), // From dist/utils (production)
|
||||
join(__dirname, '../frontend/dist'), // From src/utils (dev)
|
||||
join(process.cwd(), 'frontend/dist'), // Current working directory
|
||||
];
|
||||
|
||||
let frontendDir: string | null = null;
|
||||
for (const path of possiblePaths) {
|
||||
let isProductionBuild = false;
|
||||
|
||||
// First try to find production build
|
||||
for (const path of possibleDistPaths) {
|
||||
const resolvedPath = resolve(path);
|
||||
try {
|
||||
const { existsSync } = await import('fs');
|
||||
if (existsSync(resolvedPath)) {
|
||||
if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.html'))) {
|
||||
frontendDir = resolvedPath;
|
||||
isProductionBuild = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
@@ -43,6 +62,22 @@ export async function startReactFrontend(port: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// If no production build, try dev mode
|
||||
if (!frontendDir) {
|
||||
for (const path of possiblePaths) {
|
||||
const resolvedPath = resolve(path);
|
||||
try {
|
||||
const { existsSync } = await import('fs');
|
||||
if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'package.json'))) {
|
||||
frontendDir = resolvedPath;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Continue to next path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!frontendDir) {
|
||||
throw new Error(
|
||||
'Could not find React frontend directory. ' +
|
||||
@@ -52,6 +87,18 @@ export async function startReactFrontend(port: number): Promise<void> {
|
||||
|
||||
console.log(chalk.cyan(` Starting React frontend on port ${port}...`));
|
||||
console.log(chalk.gray(` Frontend dir: ${frontendDir}`));
|
||||
console.log(chalk.gray(` Mode: ${isProductionBuild ? 'production (static)' : 'development (vite)'}`));
|
||||
|
||||
// If production build exists, serve static files instead of running dev server
|
||||
if (isProductionBuild) {
|
||||
frontendStaticDir = frontendDir;
|
||||
console.log(chalk.green(` React frontend ready at http://localhost:${port} (static files)`));
|
||||
// Return immediately - static files will be served by the main server
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset static dir if we're in dev mode
|
||||
frontendStaticDir = null;
|
||||
|
||||
// Check if package.json exists and has dev script
|
||||
const packageJsonPath = join(frontendDir, 'package.json');
|
||||
|
||||
Reference in New Issue
Block a user