@ordojs/cli
Version:
Command-line interface for OrdoJS framework
371 lines • 12.9 kB
JavaScript
/**
* @fileoverview OrdoJS CLI - Development Server with File Watching
*
* Main development server implementation with lifecycle management and file watching.
*/
import http from 'http';
import { AddressInfo } from 'net';
import path from 'path';
import { fileExists } from '../utils/fs.js';
import { logger } from '../utils/index.js';
import { FileChangeEvent, FileWatcher } from './file-watcher.js';
import { generateHMRClientCode } from './hmr-client.js';
import { OrdoJSHMR } from './hmr.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;
fileWatcher;
hmr;
status;
actualPort;
hmrPort;
serverState;
/**
* Create a new OrdoJSDevServer instance
*
* @param options - Server options
*/
constructor(options) {
this.options = {
dir: '.',
host: 'localhost',
port: 3000,
hmr: true,
...options
};
this.server = null;
this.portManager = new PortManager(Number(this.options.port));
this.processManager = new ProcessManager();
this.fileWatcher = null;
this.hmr = null;
this.status = ServerStatus.STOPPED;
this.actualPort = 0;
this.hmrPort = this.options.hmrPort || (Number(this.options.port) + 1);
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 HMR port
*/
getHMRPort() {
return this.hmrPort;
}
/**
* 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));
// Allocate HMR port
this.hmrPort = await this.portManager.allocatePort('hmr-server', this.hmrPort);
// Create HTTP server
this.server = http.createServer(this.requestHandler.bind(this));
// Start the server
await this.startServer();
// Set up file watching and HMR if enabled
if (this.options.hmr) {
await this.setupHMR();
logger.info(`HMR enabled on port ${this.hmrPort}`);
}
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 HMR system
if (this.hmr) {
await this.hmr.stop();
this.hmr = null;
}
// Stop file watching
if (this.fileWatcher) {
await this.fileWatcher.stop();
this.fileWatcher = null;
}
// Stop the HTTP server
if (this.server) {
await this.stopServer();
}
// Clean up resources
if (this.actualPort) {
this.portManager.releasePort(this.actualPort);
}
if (this.hmrPort) {
this.portManager.releasePort(this.hmrPort);
}
// 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;
}
}
/**
* Set up HMR system and file watching
*/
async setupHMR() {
// Set up file watcher
this.fileWatcher = new FileWatcher({
watchDir: this.options.dir,
patterns: ['**/*.ordo', '**/*.ts', '**/*.js', '**/*.css', '**/*.html'],
ignorePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
debounceMs: 100
});
// Set up HMR system
this.hmr = new OrdoJSHMR({
port: this.hmrPort,
preserveState: true,
verbose: false
});
// Connect file watcher to HMR system
this.fileWatcher.on('change', async (event) => {
// Forward file changes to HMR system
await this.hmr?.handleFileChange(event);
// Also handle locally for logging
this.handleFileChange(event);
});
this.fileWatcher.on('error', this.handleFileWatcherError.bind(this));
// Start HMR system
await this.hmr.start();
// Start file watcher
await this.fileWatcher.start();
logger.success('HMR system and file watching started successfully');
}
/**
* Handle file change events
*/
async handleFileChange(event) {
logger.info(`File ${event.type}: ${event.relativePath}`);
try {
// Get affected files
const affectedFiles = this.fileWatcher?.getAffectedFiles(event.filePath) || [event.filePath];
logger.debug(`Affected files: ${affectedFiles.map(f => path.relative(this.options.dir, f)).join(', ')}`);
}
catch (error) {
logger.error(`Error handling file change: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Handle file watcher errors
*/
handleFileWatcherError(error) {
logger.error(`File watcher error: ${error.message}`);
// Attempt to restart file watching
if (this.options.hmr && this.status === ServerStatus.RUNNING) {
logger.info('Attempting to restart file watcher and HMR...');
this.setupHMR().catch(restartError => {
logger.error(`Failed to restart HMR: ${restartError instanceof Error ? restartError.message : String(restartError)}`);
});
}
}
/**
* 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
*/
requestHandler(req, res) {
const url = req.url || '/';
logger.debug(`${req.method} ${url}`);
// Inject HMR client script for HTML requests
if (this.options.hmr && (url === '/' || url.endsWith('.html'))) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>OrdoJS Dev Server</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
h1 { color: #0066cc; }
code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; }
</style>
<script>
${generateHMRClientCode(this.hmrPort)}
</script>
</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>
<p>HMR: <code>${this.options.hmr ? 'enabled' : 'disabled'}</code></p>
<p>File Watching: <code>${this.fileWatcher ? 'active' : 'inactive'}</code></p>
<p>HMR WebSocket: <code>ws://${this.options.host}:${this.hmrPort}</code></p>
</body>
</html>
`);
return;
}
// Handle other requests
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>OrdoJS Dev Server</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
h1 { color: #0066cc; }
code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; }
</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>
<p>HMR: <code>${this.options.hmr ? 'enabled' : 'disabled'}</code></p>
<p>File Watching: <code>${this.fileWatcher ? 'active' : 'inactive'}</code></p>
</body>
</html>
`);
}
}
//# sourceMappingURL=server-with-watcher.js.map