import * as path from 'path'; import * as fs from 'fs-extra'; import { spawn, ChildProcess } from 'child_process'; import { Log } from '@spine/logger'; import { config } from '@erect/core/config/index'; import { Compiler } from 'webpack'; import { notify } from '@erect/server/utils/notify'; import { getBuildPath } from '@erect/core/paths'; import { plugin } from './plugin'; export class HotNodeServer { server: ChildProcess | undefined; serverCompiling = false; watcher: Compiler.Watching; disposing = false; clientCompiling = false; constructor(name: string, compiler: Compiler, clientCompiler: Compiler, log: Log, restarted = false) { let rebuilding = false; let compileWaitTimer: any; let startTimer: any; const compiledFiles: string[] = [ path.resolve( getBuildPath(), config.bundles.server.buildOutputDir, 'index.js', ) ]; const buildEnv = fs.readJSONSync( path.resolve( getBuildPath(), config.bundles.server.buildOutputDir, 'buildEnv.json', ), ); let restartedServer = false; const restartServer = () => { if (this.server != null) { restartedServer = true; this.server.once('exit', () => { log.info('Restarting server...'); startServer(); }); this.server.kill(); } else { log.info('Starting server...'); startServer(); } }; const startServer = () => { clearTimeout(startTimer); startTimer = setTimeout(() => { restartedServer = false; const args = [ '-r', 'tsconfig-paths/register', ]; const files = compiledFiles.slice(0); while (files.length > 0) { const compiledFile = files.shift(); if (compiledFile != null) { if (files.length > 0) { args.push('-r'); } args.push(compiledFile); } } args.push('--color'); const newServer = spawn('node', args, { stdio: 'inherit', env: { ...process.env, ...buildEnv, PATH: process.env.PATH, NODE_ENV: 'development', ...(process.env.ERECT_DIR != null && { ERECT_DIR: process.env.ERECT_DIR, }), ERECT_COMMAND: undefined, SERVER_RESTART: JSON.stringify(restarted || restartedServer), }, }); newServer.on('error', (error) => { console.error(error.stack); }); newServer.once('exit', (code, signal) => { if (!this.disposing) { if (code > 0) { notify('✖️ An errorFallback has occured, check the terminal'); } else if (!restartedServer) { notify('✖️ Server has ended unexpectedly, check the terminal'); } if (code > 0 || !restartedServer) { process.emit('SIGTERM'); } restartedServer = false; } }); this.server = newServer; }, 10); }; // We want our node server bundles to only start after a successful client // build. This avoids any issues with node server bundles depending on // client bundle assets. const waitForClientThenStartServer = () => { clearTimeout(compileWaitTimer); if (this.clientCompiling && this.serverCompiling) { compileWaitTimer = setTimeout(waitForClientThenStartServer, 50); } else { restartServer(); } }; clientCompiler.hooks.compile.tap(plugin, () => { this.clientCompiling = true; }); clientCompiler.hooks.done.tap(plugin, (stats: any) => { if (!stats.hasErrors()) { this.clientCompiling = false; } }); let compileTimer: any; compiler.hooks.beforeCompile.tap(plugin, (...args: any[]) => { this.serverCompiling = true; clearTimeout(compileTimer); compileTimer = setTimeout(() => { if (rebuilding) { log.info('Rebuilding bundle...'); } else { rebuilding = true; log.info('Building new bundle...'); } }, 10); }); compiler.hooks.done.tap(plugin, (stats: any) => { this.serverCompiling = false; if (this.disposing) { return; } try { if (stats.hasErrors()) { log.error('Build failed.'); notify('Build failed, check the console for more information.'); console.error(stats.toString()); process.emit('SIGTERM'); return; } waitForClientThenStartServer(); } catch (err) { log.error('Failed to start'); notify('Failed to start, please check the console for more information.'); console.error(err); } }); // Lets start the compiler. this.watcher = compiler.watch({}, (err) => { if (err != null) { log.error('Build failed.'); notify('Build failed, check the console for more information.'); console.error(err); // process.emit('SIGTERM'); return; } }); } async dispose() { this.disposing = true; await new Promise((resolve) => { if (this.server != null) { this.server.on('exit', () => { resolve(); }); this.server.kill(); } else { resolve(); } }); await new Promise((resolve) => { this.watcher.close(() => { resolve(); }); }); } }