fusion-cli/build/dev-runtime.js (198 lines of code) (raw):

/** Copyright (c) 2018 Uber Technologies, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* eslint-env node */ const path = require('path'); const http = require('http'); const EventEmitter = require('events'); const getPort = require('get-port'); const {spawn} = require('child_process'); const {promisify} = require('util'); const openUrl = require('react-dev-utils/openBrowser'); const httpProxy = require('http-proxy'); const renderError = require('./server-error').renderError; // mechanism to allow a running proxy server to wait for a child process server to start function Lifecycle() { const emitter = new EventEmitter(); const state = {started: false, error: undefined}; let listening = false; return { start: () => { state.started = true; state.error = undefined; emitter.emit('message'); }, stop: () => { state.started = false; }, error: error => { state.error = error; // The error listener may emit before we call wait. // Make sure that we're listening before attempting to emit. if (listening) { emitter.emit('message', error); } }, wait: () => { return new Promise((resolve, reject) => { if (state.started) resolve(); else if (state.error) reject(state.error); else { listening = true; emitter.once('message', (error /*: Error */) => { if (error) { listening = false; return reject(error); } resolve(); }); } }); }, }; } /*:: type DevRuntimeType = { run: () => any, start: () => any, stop: () => any, invalidate: () => void }; */ module.exports.DevelopmentRuntime = function( { port, dir = '.', noOpen, middleware = (req, res, next) => next(), debug = false, } /*: any */ ) /*: DevRuntimeType */ { const lifecycle = new Lifecycle(); const state = { server: null, proc: null, proxy: null, }; this.run = async function reloadProc() { const childPort = await getPort(); const command = ` process.on('SIGTERM', () => process.exit()); process.on('SIGINT', () => process.exit()); const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); const logErrors = e => { //eslint-disable-next-line no-console console.error(chalk.red(e.stack)) } const logAndSend = e => { logErrors(e); process.send({event: 'error', payload: { message: e.message, name: e.name, stack: e.stack, type: e.type }}); } const entry = path.resolve( '.fusion/dist/development/server/server-main.js' ); if (fs.existsSync(entry)) { try { const {start} = require(entry); start({port: ${childPort}}) .then(() => { process.send({event: 'started'}) }) .catch(logAndSend); // handle server bootstrap errors (e.g. port already in use) } catch (e) { logAndSend(e); // handle app top level errors } } else { logAndSend(new Error(\`No entry found at \${entry}\`)); } `; killProc(); return new Promise((resolve, reject) => { function handleChildServerCrash(err) { lifecycle.stop(); killProc(); reject(err); } const args = ['-e', command]; if (debug) args.push('--inspect-brk'); state.proxy = httpProxy.createProxyServer({ target: { host: 'localhost', port: childPort, }, }); // $FlowFixMe state.proc = spawn('node', args, { cwd: path.resolve(process.cwd(), dir), stdio: ['inherit', 'inherit', 'inherit', 'ipc'], }); // $FlowFixMe state.proc.on('error', handleChildServerCrash); // $FlowFixMe state.proc.on('exit', handleChildServerCrash); // $FlowFixMe state.proc.on('message', message => { if (message.event === 'started') { lifecycle.start(); resolve(); } if (message.event === 'error') { lifecycle.error(message.payload); killProc(); reject(new Error('Received error message from server')); } }); }); }; this.invalidate = () => lifecycle.stop(); function killProc() { if (state.proc) { lifecycle.stop(); state.proc.removeAllListeners(); state.proc.kill(); state.proc = null; } if (state.proxy) { state.proxy.close(); state.proxy = null; } } this.start = async function start() { // $FlowFixMe state.server = http.createServer((req, res) => { middleware(req, res, async () => { lifecycle.wait().then( () => { // $FlowFixMe state.proxy.web(req, res, e => { if (res.finished) return; res.write(renderError(e)); res.end(); }); }, error => { if (res.finished) return; res.write(renderError(error)); res.end(); } ); }); }); state.server.on('upgrade', (req, socket, head) => { socket.on('error', e => { socket.destroy(); }); lifecycle.wait().then( () => { // $FlowFixMe state.proxy.ws(req, socket, head, (/*e*/) => { socket.destroy(); }); }, () => { // Destroy the socket to terminate the websocket request if the child process has issues socket.destroy(); } ); }); // $FlowFixMe const listen = promisify(state.server.listen.bind(state.server)); return listen(port).then(() => { const url = `http://localhost:${port}`; if (!noOpen) openUrl(url); }); }; this.stop = () => { killProc(); if (state.server) { state.server.close(); state.server = null; // ensure we can call .run() again after stopping } }; return this; };