@ordojs/cli
Version:
Command-line interface for OrdoJS framework
458 lines • 15.7 kB
JavaScript
/**
* @fileoverview OrdoJS CLI - Development Server
*
* Main development server implementation with lifecycle management.
*/
import { readdir } from 'fs/promises';
import http from 'http';
import path from 'path';
import { fileExists, readFile } from '../utils/fs.js';
import { logger } from '../utils/index.js';
import { PortManager } from './port-manager.js';
import { ProcessManager } from './process-manager.js';
/**
* Server status enum
*/
export var ServerStatus;
(function (ServerStatus) {
ServerStatus["STOPPED"] = "stopped";
ServerStatus["STARTING"] = "starting";
ServerStatus["RUNNING"] = "running";
ServerStatus["STOPPING"] = "stopping";
ServerStatus["ERROR"] = "error";
ServerStatus["RESTARTING"] = "restarting";
})(ServerStatus || (ServerStatus = {}));
/**
* OrdoJSDevServer class for managing the development server lifecycle
*/
export class OrdoJSDevServer {
options;
server;
portManager;
processManager;
status;
actualPort;
serverState;
/**
* Create a new OrdoJSDevServer instance
*
* @param options - Server options
*/
constructor(options) {
this.options = {
...options,
dir: options.dir || '.',
host: options.host || 'localhost',
port: options.port || 3000,
hmr: options.hmr !== false
};
this.server = null;
this.portManager = new PortManager(Number(this.options.port));
this.processManager = new ProcessManager();
this.status = ServerStatus.STOPPED;
this.actualPort = 0;
this.serverState = {};
}
/**
* Get the current server status
*/
getStatus() {
return this.status;
}
/**
* Get the actual port the server is running on
*/
getPort() {
return this.actualPort;
}
/**
* Get the current server state
*
* @returns The current server state
*/
getServerState() {
return { ...this.serverState };
}
/**
* Start the development server
*/
async start() {
if (this.status === ServerStatus.RUNNING) {
logger.info('Server is already running');
return;
}
if (this.status === ServerStatus.STARTING) {
logger.info('Server is already starting');
return;
}
this.status = ServerStatus.STARTING;
logger.info('Starting development server...');
try {
// Validate the directory exists
const dirExists = await fileExists(this.options.dir);
if (!dirExists) {
throw new Error(`Directory not found: ${this.options.dir}`);
}
// Allocate a port
this.actualPort = await this.portManager.allocatePort('dev-server', Number(this.options.port));
// Create HTTP server
this.server = http.createServer(this.requestHandler.bind(this));
// Start the server
await this.startServer();
// Set up HMR if enabled
if (this.options.hmr) {
// HMR setup will be implemented in task 9.2
logger.info('HMR enabled (implementation pending)');
}
this.status = ServerStatus.RUNNING;
logger.success(`Development server running at http://${this.options.host}:${this.actualPort}`);
}
catch (error) {
this.status = ServerStatus.ERROR;
logger.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Stop the development server
*/
async stop() {
if (this.status === ServerStatus.STOPPED) {
logger.info('Server is already stopped');
return;
}
if (this.status === ServerStatus.STOPPING) {
logger.info('Server is already stopping');
return;
}
this.status = ServerStatus.STOPPING;
logger.info('Stopping development server...');
try {
// Stop the HTTP server
if (this.server) {
await this.stopServer();
}
// Clean up resources
if (this.actualPort) {
this.portManager.releasePort(this.actualPort);
}
// Clean up child processes
await this.processManager.cleanup();
this.status = ServerStatus.STOPPED;
logger.success('Development server stopped');
}
catch (error) {
this.status = ServerStatus.ERROR;
logger.error(`Failed to stop server: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Restart the development server
*/
async restart() {
if (this.status === ServerStatus.RESTARTING) {
logger.info('Server is already restarting');
return;
}
this.status = ServerStatus.RESTARTING;
logger.info('Restarting development server...');
try {
// Preserve the server state before stopping
const preservedState = { ...this.serverState };
// Stop the server but keep the state
await this.stop();
// Restore the preserved state
this.serverState = preservedState;
// Start the server with the preserved state
await this.start();
logger.success('Development server restarted successfully');
}
catch (error) {
this.status = ServerStatus.ERROR;
logger.error(`Failed to restart server: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Start the HTTP server
*/
startServer() {
return new Promise((resolve, reject) => {
if (!this.server) {
reject(new Error('Server not initialized'));
return;
}
// Handle server errors
this.server.once('error', (error) => {
const e = error;
if (e.code === 'EADDRINUSE') {
reject(new Error(`Port ${this.actualPort} is already in use`));
}
else {
reject(error);
}
});
// Start listening
this.server.listen(this.actualPort, this.options.host, () => {
const address = this.server?.address();
this.actualPort = address.port;
resolve();
});
});
}
/**
* Stop the HTTP server
*/
stopServer() {
return new Promise((resolve, reject) => {
if (!this.server) {
resolve();
return;
}
this.server.close((error) => {
if (error) {
reject(error);
}
else {
this.server = null;
resolve();
}
});
});
}
/**
* HTTP request handler
*/
async requestHandler(req, res) {
const url = req.url || '/';
logger.debug(`${req.method} ${url}`);
try {
// Handle different file types
if (url.endsWith('.ordo')) {
await this.handleOrdoFile(req, res, url);
}
else if (url === '/' || url === '/index.html') {
await this.handleIndex(req, res);
}
else {
await this.handleStaticFile(req, res, url);
}
}
catch (error) {
logger.error(`Request handler error: ${error instanceof Error ? error.message : String(error)}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
}
/**
* Handle OrdoJS file compilation
*/
async handleOrdoFile(req, res, url) {
const filePath = path.join(this.options.dir, url);
try {
// Read the OrdoJS file
const source = await readFile(filePath);
// Compile the OrdoJS file to HTML/JS
const html = this.compileOrdoToHTML(source, path.basename(filePath, '.ordo'));
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
}
catch (error) {
logger.error(`Failed to compile OrdoJS file: ${error instanceof Error ? error.message : String(error)}`);
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>Compilation Error</title></head>
<body>
<h1>OrdoJS Compilation Error</h1>
<p>Failed to compile ${url}: ${error instanceof Error ? error.message : String(error)}</p>
</body>
</html>
`);
}
}
/**
* Handle index page
*/
async handleIndex(req, res) {
// First try to serve public/index.html
const indexHtmlPath = path.join(this.options.dir, 'public/index.html');
try {
const exists = await fileExists(indexHtmlPath);
if (exists) {
// Serve the index.html file
const content = await readFile(indexHtmlPath);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
return;
}
}
catch (error) {
// Continue to fallback
}
// Fallback: Look for app.ordo in the current directory
const appOrdoPath = path.join(this.options.dir, 'src/app.ordo');
try {
const exists = await fileExists(appOrdoPath);
if (exists) {
// Redirect to the app.ordo file
res.writeHead(302, { 'Location': '/src/app.ordo' });
res.end();
}
else {
// Show directory listing or default page
await this.showDirectoryListing(req, res);
}
}
catch (error) {
await this.showDirectoryListing(req, res);
}
}
/**
* Handle static files
*/
async handleStaticFile(req, res, url) {
const filePath = path.join(this.options.dir, url);
try {
const exists = await fileExists(filePath);
if (!exists) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('File not found');
return;
}
const content = await readFile(filePath);
const ext = path.extname(filePath);
let contentType = 'text/plain';
switch (ext) {
case '.html':
contentType = 'text/html';
break;
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'application/javascript';
break;
case '.json':
contentType = 'application/json';
break;
case '.png':
contentType = 'image/png';
break;
case '.jpg':
case '.jpeg':
contentType = 'image/jpeg';
break;
case '.gif':
contentType = 'image/gif';
break;
case '.svg':
contentType = 'image/svg+xml';
break;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
}
/**
* Show directory listing
*/
async showDirectoryListing(req, res) {
try {
const files = await readdir(this.options.dir);
const ordoFiles = files.filter(file => file.endsWith('.ordo'));
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>OrdoJS Development Server</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
h1 { color: #0066cc; }
.file-list { list-style: none; padding: 0; }
.file-list li { margin: 0.5rem 0; }
.file-list a { color: #0066cc; text-decoration: none; }
.file-list a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>OrdoJS Development Server</h1>
<p>Server is running at <code>http://${this.options.host}:${this.actualPort}</code></p>
<p>Serving directory: <code>${path.resolve(this.options.dir)}</code></p>
${ordoFiles.length > 0 ? `
<h2>Available OrdoJS Files:</h2>
<ul class="file-list">
${ordoFiles.map(file => `<li><a href="/${file}">${file}</a></li>`).join('')}
</ul>
` : '<p>No .ordo files found in the current directory.</p>'}
</body>
</html>
`);
}
catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Failed to read directory');
}
}
/**
* Compile OrdoJS to HTML
*/
compileOrdoToHTML(source, componentName) {
try {
// Simple compilation for now - just wrap the content in HTML
// This is a basic implementation that will be enhanced
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${componentName} - OrdoJS</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; }
.component { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; }
.source { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }
pre { margin: 0; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>${componentName}</h1>
<div class="component">
<h2>Compiled Component</h2>
<p>This is a placeholder for the compiled OrdoJS component.</p>
<p>The full compilation pipeline will be implemented in upcoming tasks.</p>
</div>
<div class="source">
<h3>Source Code:</h3>
<pre><code>${source.replace(/</g, '<').replace(/>/g, '>')}</code></pre>
</div>
<script>
console.log('OrdoJS component: ${componentName}');
console.log('Source:', \`${source.replace(/`/g, '\\`')}\`);
</script>
</body>
</html>
`;
}
catch (error) {
return `
<!DOCTYPE html>
<html>
<head><title>Compilation Error</title></head>
<body>
<h1>OrdoJS Compilation Error</h1>
<p>Failed to compile component: ${error instanceof Error ? error.message : String(error)}</p>
</body>
</html>
`;
}
}
}
//# sourceMappingURL=server.js.map