UNPKG

6.26 kBJavaScriptView Raw
1/** Copyright (c) 2018 Uber Technologies, Inc.
2 *
3 * This source code is licensed under the MIT license found in the
4 * LICENSE file in the root directory of this source tree.
5 *
6 * @flow
7 */
8
9/* eslint-env node */
10
11const path = require('path');
12const http = require('http');
13const EventEmitter = require('events');
14const getPort = require('get-port');
15const {spawn} = require('child_process');
16const {promisify} = require('util');
17const openUrl = require('react-dev-utils/openBrowser');
18const httpProxy = require('http-proxy');
19
20const renderError = require('./server-error').renderError;
21
22// mechanism to allow a running proxy server to wait for a child process server to start
23function Lifecycle() {
24 const emitter = new EventEmitter();
25 const state = {started: false, error: undefined};
26 let listening = false;
27 return {
28 start: () => {
29 state.started = true;
30 state.error = undefined;
31 emitter.emit('message');
32 },
33 stop: () => {
34 state.started = false;
35 },
36 error: error => {
37 state.error = error;
38 // The error listener may emit before we call wait.
39 // Make sure that we're listening before attempting to emit.
40 if (listening) {
41 emitter.emit('message', error);
42 }
43 },
44 wait: () => {
45 return new Promise((resolve, reject) => {
46 if (state.started) resolve();
47 else if (state.error) reject(state.error);
48 else {
49 listening = true;
50 emitter.once('message', (error /*: Error */) => {
51 if (error) {
52 listening = false;
53 return reject(error);
54 }
55
56 resolve();
57 });
58 }
59 });
60 },
61 };
62}
63
64/*::
65type DevRuntimeType = {
66 run: () => any,
67 start: () => any,
68 stop: () => any,
69 invalidate: () => void
70};
71*/
72
73module.exports.DevelopmentRuntime = function(
74 {
75 port,
76 dir = '.',
77 noOpen,
78 middleware = (req, res, next) => next(),
79 debug = false,
80 } /*: any */
81) /*: DevRuntimeType */ {
82 const lifecycle = new Lifecycle();
83 const state = {
84 server: null,
85 proc: null,
86 proxy: null,
87 };
88
89 this.run = async function reloadProc() {
90 const childPort = await getPort();
91 const command = `
92 process.on('SIGTERM', () => process.exit());
93 process.on('SIGINT', () => process.exit());
94
95 const fs = require('fs');
96 const path = require('path');
97 const chalk = require('chalk');
98
99 const logErrors = e => {
100 //eslint-disable-next-line no-console
101 console.error(chalk.red(e.stack))
102 }
103
104 const logAndSend = e => {
105 logErrors(e);
106 process.send({event: 'error', payload: {
107 message: e.message,
108 name: e.name,
109 stack: e.stack,
110 type: e.type
111 }});
112 }
113
114 const entry = path.resolve(
115 '.fusion/dist/development/server/server-main.js'
116 );
117
118 if (fs.existsSync(entry)) {
119 try {
120 const {start} = require(entry);
121 start({port: ${childPort}})
122 .then(() => {
123 process.send({event: 'started'})
124 })
125 .catch(logAndSend); // handle server bootstrap errors (e.g. port already in use)
126 }
127 catch (e) {
128 logAndSend(e); // handle app top level errors
129 }
130 }
131 else {
132 logAndSend(new Error(\`No entry found at \${entry}\`));
133 }
134 `;
135
136 killProc();
137
138 return new Promise((resolve, reject) => {
139 function handleChildServerCrash(err) {
140 lifecycle.stop();
141 killProc();
142 reject(err);
143 }
144 const args = ['-e', command];
145 if (debug) args.push('--inspect-brk');
146
147 state.proxy = httpProxy.createProxyServer({
148 target: {
149 host: 'localhost',
150 port: childPort,
151 },
152 });
153
154 // $FlowFixMe
155 state.proc = spawn('node', args, {
156 cwd: path.resolve(process.cwd(), dir),
157 stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
158 });
159 // $FlowFixMe
160 state.proc.on('error', handleChildServerCrash);
161 // $FlowFixMe
162 state.proc.on('exit', handleChildServerCrash);
163 // $FlowFixMe
164 state.proc.on('message', message => {
165 if (message.event === 'started') {
166 lifecycle.start();
167 resolve();
168 }
169 if (message.event === 'error') {
170 lifecycle.error(message.payload);
171 killProc();
172 reject(new Error('Received error message from server'));
173 }
174 });
175 });
176 };
177
178 this.invalidate = () => lifecycle.stop();
179
180 function killProc() {
181 if (state.proc) {
182 lifecycle.stop();
183 state.proc.removeAllListeners();
184 state.proc.kill();
185 state.proc = null;
186 }
187 if (state.proxy) {
188 state.proxy.close();
189 state.proxy = null;
190 }
191 }
192
193 this.start = async function start() {
194 // $FlowFixMe
195 state.server = http.createServer((req, res) => {
196 middleware(req, res, async () => {
197 lifecycle.wait().then(
198 () => {
199 // $FlowFixMe
200 state.proxy.web(req, res, e => {
201 if (res.finished) return;
202
203 res.write(renderError(e));
204 res.end();
205 });
206 },
207 error => {
208 if (res.finished) return;
209
210 res.write(renderError(error));
211 res.end();
212 }
213 );
214 });
215 });
216
217 state.server.on('upgrade', (req, socket, head) => {
218 socket.on('error', e => {
219 socket.destroy();
220 });
221 lifecycle.wait().then(
222 () => {
223 // $FlowFixMe
224 state.proxy.ws(req, socket, head, (/*e*/) => {
225 socket.destroy();
226 });
227 },
228 () => {
229 // Destroy the socket to terminate the websocket request if the child process has issues
230 socket.destroy();
231 }
232 );
233 });
234
235 // $FlowFixMe
236 const listen = promisify(state.server.listen.bind(state.server));
237 return listen(port).then(() => {
238 const url = `http://localhost:${port}`;
239 if (!noOpen) openUrl(url);
240 });
241 };
242
243 this.stop = () => {
244 killProc();
245 if (state.server) {
246 state.server.close();
247 state.server = null; // ensure we can call .run() again after stopping
248 }
249 };
250
251 return this;
252};