gensx
Version:
`GenSX command line tools.
254 lines • 12.6 kB
JavaScript
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