All files / cli/src/dev-server index.js

70.27% Statements 26/37
66.66% Branches 14/21
87.5% Functions 7/8
66.66% Lines 22/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132                                                                                                            4x   4x 4x   4x 4x 4x 4x 4x       4x   4x 4x 4x 3x 3x     1x           1x                                           4x               4x                 4x       4x 4x 4x        
/**
 * Coherent Dev Server
 *
 * Orchestrator that boots:
 *   - An HTTP server serving static files under `root` (with HMR
 *     client injection on HTML responses)
 *   - A WebSocket server sharing the HTTP port, used to broadcast
 *     HMR messages to connected browser clients
 *   - A chokidar file watcher that emits HMR update messages on
 *     edits to files under `root`
 *
 * @module @coherent.js/cli/dev-server
 */
 
import { createServer } from 'node:http';
import picocolors from 'picocolors';
import { createHmrServer } from './hmr-server.js';
import { createFileWatcher } from './file-watcher.js';
import { createStaticHandler } from './static-handler.js';
 
/**
 * @typedef {Object} DevServerOptions
 * @property {string} root - Absolute path to the project root.
 * @property {number} [port=3000] - Port to listen on. `0` picks a random free port (useful for tests).
 * @property {string} [host='localhost'] - Host interface to bind.
 * @property {boolean} [open=false] - Open the default browser to the served URL after start.
 * @property {boolean} [log=true] - Emit startup / change log lines to stdout.
 */
 
/**
 * @typedef {Object} DevServer
 * @property {number} port - The actual port the HTTP server is listening on.
 * @property {string} host - The host the HTTP server is bound to.
 * @property {() => Promise<void>} close - Shut down HTTP server, WS server, and file watcher.
 */
 
/**
 * Start the Coherent dev server and return a handle for graceful shutdown.
 *
 * Resolves once the HTTP server is listening AND the file watcher's
 * initial scan is complete — callers can immediately connect a WS
 * client and trust that touching a file will fire an HMR message.
 *
 * @param {DevServerOptions} options
 * @returns {Promise<DevServer>}
 */
export async function startDevServer(options) {
  const {
    root,
    port = 3000,
    host = 'localhost',
    open = false,
    log = true,
    hmr = true,
  } = options;
 
  const handler = createStaticHandler({ root, hmr });
  const httpServer = createServer(handler);
 
  await new Promise((resolve, reject) => {
    httpServer.once('error', reject);
    httpServer.listen(port, host, () => {
      httpServer.removeListener('error', reject);
      resolve();
    });
  });
 
  const actualPort = httpServer.address().port;
 
  let hmrServer = null;
  let watcher = null;
  if (hmr) {
    hmrServer = createHmrServer(httpServer);
    watcher = await createFileWatcher({
      root,
      onChange: (change) => {
        hmrServer.broadcast({
          type: 'hmr-update',
          filePath: change.filePath,
          webPath: change.webPath,
          updateType: change.updateType,
        });
        Iif (log) {
          console.log(picocolors.cyan('[hmr]'), change.updateType, change.webPath);
        }
      },
      onError: (err) => {
        hmrServer.broadcast({
          type: 'hmr-error',
          error: {
            message: err.message,
            file: null,
            line: null,
            column: null,
            stack: err.stack,
          },
        });
        if (log) {
          console.warn(picocolors.yellow('[hmr] watcher error:'), err.message);
        }
      },
    });
  }
 
  Iif (log) {
    console.log(picocolors.green('✅ Coherent dev server ready'));
    console.log(picocolors.cyan('🌐 Local:'), `http://${host}:${actualPort}`);
    if (!hmr) {
      console.log(picocolors.gray('   HMR: disabled (--no-hmr)'));
    }
  }
 
  Iif (open) {
    try {
      const { default: openModule } = await import('open');
      await openModule(`http://${host}:${actualPort}`);
    } catch {
      // 'open' is optional — silently no-op if missing
    }
  }
 
  return {
    port: actualPort,
    host,
    async close() {
      if (watcher) await watcher.close();
      if (hmrServer) hmrServer.close();
      await new Promise((resolve) => httpServer.close(() => resolve()));
    },
  };
}