UNPKG

gensx

Version:
254 lines 12.6 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path, { resolve } from "node:path"; import { Box, Text, useApp } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; import * as ts from "typescript"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { LoadingSpinner } from "../components/LoadingSpinner.js"; import { createServer } from "../dev-server.js"; import { generateSchema } from "../utils/schema.js"; export const StartUI = ({ file, options }) => { const { exit } = useApp(); const [phase, setPhase] = useState("initial"); const [error, setError] = useState(null); const [currentServer, setCurrentServer] = useState(null); const currentServerRef = useRef(null); const [_schemas, setSchemas] = useState({}); const isRebuildingRef = useRef(false); const [serverLogs, setServerLogs] = useState([]); const [isRebuilding, setIsRebuilding] = useState(false); // Keep currentServerRef synchronized with currentServer state useEffect(() => { currentServerRef.current = currentServer; }, [currentServer]); const handleError = useCallback((err) => { const message = err instanceof Error ? err.message : String(err); setError(message); setPhase("error"); setTimeout(() => { exit(); }, 100); }, [exit]); const compileTypeScript = useCallback((tsFile) => { // Find and parse tsconfig.json const tsconfigPath = resolve(process.cwd(), "tsconfig.json"); if (!existsSync(tsconfigPath)) { throw new Error("Could not find tsconfig.json"); } let tsconfig; try { const tsconfigContent = readFileSync(tsconfigPath, "utf-8"); tsconfig = ts.parseJsonConfigFileContent(JSON.parse(tsconfigContent), ts.sys, process.cwd(), { incremental: false, noEmit: false, }); tsconfig.options.incremental = false; tsconfig.options.noEmit = false; tsconfig.options.outDir ??= ".gensx/dist"; tsconfig.options.target = ts.ScriptTarget.ESNext; tsconfig.options.module = ts.ModuleKind.NodeNext; tsconfig.options.moduleResolution = ts.ModuleResolutionKind.NodeNext; } catch (error) { throw new Error(`Failed to parse tsconfig.json: ${error instanceof Error ? error.message : String(error)}`); } // Create TypeScript program const program = ts.createProgram([tsFile], tsconfig.options); const sourceFile = program.getSourceFile(tsFile); if (!sourceFile) { throw new Error(`Could not find source file: ${tsFile}`); } // Get the output directory from tsconfig or use default const configOutDir = tsconfig.options.outDir ?? ".gensx/dist"; const absoluteOutDir = resolve(process.cwd(), configOutDir); // Ensure output directory exists if (!existsSync(absoluteOutDir)) { mkdirSync(absoluteOutDir, { recursive: true }); } // Compile the file const result = program.emit(); const diagnostics = ts .getPreEmitDiagnostics(program) .concat(result.diagnostics); if (diagnostics.length > 0) { const formattedDiagnostics = ts.formatDiagnostics(diagnostics, { getCurrentDirectory: () => process.cwd(), getCanonicalFileName: (fileName) => fileName, getNewLine: () => "\n", }); throw new Error(`TypeScript compilation failed:\n${formattedDiagnostics}`); } // Get the actual output path that TypeScript generated // If rootDir is specified in tsconfig, use it, otherwise use the source file's directory const rootDir = tsconfig.options.rootDir ?? path.dirname(tsFile); const relativeToRootDir = path.relative(rootDir, tsFile); const actualOutputPath = path.join(absoluteOutDir, relativeToRootDir.replace(/\.tsx?$/, ".js")); return actualOutputPath; }, []); const buildAndStartServer = useCallback(async () => { if (isRebuildingRef.current) { return; } isRebuildingRef.current = true; setIsRebuilding(true); try { if (currentServerRef.current) { // Add restart message setServerLogs((logs) => [ ...logs, "", "🔄 Restarting server due to code changes...", "", ]); await currentServerRef.current.stop(); // Add a short delay to allow the OS to release the port await new Promise((resolve) => setTimeout(resolve, 250)); currentServerRef.current = null; setCurrentServer(null); } setPhase("compiling"); const jsPath = compileTypeScript(file); setPhase("generatingSchema"); const newSchemas = generateSchema(file); setSchemas(newSchemas); const schemaFile = resolve(process.cwd(), ".gensx", "schema.json"); // Ensure .gensx directory exists const schemaDir = path.dirname(schemaFile); if (!existsSync(schemaDir)) { mkdirSync(schemaDir, { recursive: true }); } writeFileSync(schemaFile, JSON.stringify(newSchemas, null, 2)); setPhase("starting"); const fileUrl = `file://${jsPath}?update=${Date.now().toString()}`; const workflows = (await import(fileUrl)); setServerLogs([]); const server = createServer(workflows, { port: options.port ?? 1337, logger: { info: (msg) => { setServerLogs((logs) => [...logs, msg]); }, error: (msg, err) => { const errorStr = err instanceof Error ? err.message : String(err); setServerLogs((logs) => [ ...logs, `${msg}${err ? `: ${errorStr}` : ""}`, ]); }, warn: (msg) => { setServerLogs((logs) => [...logs, msg]); }, }, }, newSchemas); try { const serverInstance = server.start(); currentServerRef.current = serverInstance; setCurrentServer(serverInstance); setPhase("running"); // Add success message after restart if (serverLogs.length > 0) { // Only show for restarts, not first startup setServerLogs((logs) => [ ...logs, "", "✅ Server restarted successfully!", `🚀 Server running at http://localhost:${options.port ?? 1337}`, "", ]); } } catch (err) { // Add visible error message setServerLogs((logs) => [ ...logs, "", "❌ Error restarting server:", err instanceof Error ? err.message : String(err), "", ]); // If this is an EADDRINUSE error, try to recover by forcibly stopping any server that might be lingering if (err instanceof Error && err.message.includes("EADDRINUSE")) { // Wait a bit longer to allow for port to potentially be released await new Promise((resolve) => setTimeout(resolve, 1000)); // Force reset our references currentServerRef.current = null; setCurrentServer(null); } throw err; // rethrow to the outer catch block } } catch (err) { handleError(err); } finally { isRebuildingRef.current = false; setIsRebuilding(false); } }, [ file, options.port, compileTypeScript, handleError, setPhase, setCurrentServer, setSchemas, setServerLogs, exit, ]); useEffect(() => { void buildAndStartServer(); // Set up file watching const directoryToWatch = path.dirname(resolve(process.cwd(), file)); let rebuildTimer = null; const triggerRebuild = () => { if (rebuildTimer) { clearTimeout(rebuildTimer); } // Set rebuilding state to show spinner setIsRebuilding(true); rebuildTimer = setTimeout(() => { void buildAndStartServer(); }, 1000); }; const fs = import("node:fs"); void fs .then(({ watch }) => { const watcher = watch(directoryToWatch, (_eventType, filename) => { if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx"))) { triggerRebuild(); } }); return () => { watcher.close(); if (rebuildTimer) { clearTimeout(rebuildTimer); } if (currentServerRef.current) { currentServerRef.current.stop().catch((err) => { console.error("Error stopping server:", err); }); } }; }) .catch((err) => { handleError(err); }); return () => { if (currentServerRef.current) { currentServerRef.current.stop().catch((err) => { console.error("Error stopping server:", err); }); } }; }, [file, options.port, buildAndStartServer, handleError]); if (error) { return _jsx(ErrorMessage, { message: error }); } return (_jsx(Box, { flexDirection: "column", children: isRebuilding ? (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { message: "Changes detected, rebuilding..." }) })) : (_jsxs(_Fragment, { children: [(phase === "initial" || phase === "compiling" || phase === "generatingSchema" || phase === "starting") && (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { message: "Starting dev server..." }) })), phase === "running" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Box, { width: 32, children: _jsx(Text, { children: "\uD83D\uDE80 GenSX Dev Server running at" }) }), _jsx(Box, { children: _jsxs(Text, { color: "cyan", bold: true, children: ["http://localhost:", options.port ?? 1337] }) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 32, children: _jsx(Text, { children: "\uD83E\uDDEA Swagger UI available at" }) }), _jsx(Box, { children: _jsxs(Text, { color: "cyan", bold: true, children: ["http://localhost:", options.port ?? 1337, "/swagger-ui"] }) })] }), currentServer && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Available workflows:" }), _jsx(Box, { borderStyle: "single", borderColor: "gray", borderTop: false, borderLeft: false, borderRight: false, paddingBottom: 0, marginBottom: 0 }), currentServer.getWorkflows().map((workflow) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 24, children: _jsxs(Text, { children: [workflow.name, ":"] }) }), _jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: workflow.url }) })] }, workflow.name)))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["Listening for changes... ", new Date().toLocaleTimeString()] }) }), serverLogs.length > 0 && (_jsx(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: _jsx(Box, { children: _jsx(Text, { children: serverLogs.slice(-20).join("\n") }) }) }))] }))] })) })); }; //# sourceMappingURL=start.js.map