1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | const path = require('path');
|
12 | const http = require('http');
|
13 | const EventEmitter = require('events');
|
14 | const getPort = require('get-port');
|
15 | const {spawn} = require('child_process');
|
16 | const {promisify} = require('util');
|
17 | const openUrl = require('react-dev-utils/openBrowser');
|
18 | const httpProxy = require('http-proxy');
|
19 |
|
20 | const renderError = require('./server-error').renderError;
|
21 |
|
22 |
|
23 | function 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 |
|
39 |
|
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 ) => {
|
51 | if (error) {
|
52 | listening = false;
|
53 | return reject(error);
|
54 | }
|
55 |
|
56 | resolve();
|
57 | });
|
58 | }
|
59 | });
|
60 | },
|
61 | };
|
62 | }
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | module.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 |
|
155 | state.proc = spawn('node', args, {
|
156 | cwd: path.resolve(process.cwd(), dir),
|
157 | stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
158 | });
|
159 |
|
160 | state.proc.on('error', handleChildServerCrash);
|
161 |
|
162 | state.proc.on('exit', handleChildServerCrash);
|
163 |
|
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 |
|
195 | state.server = http.createServer((req, res) => {
|
196 | middleware(req, res, async () => {
|
197 | lifecycle.wait().then(
|
198 | () => {
|
199 |
|
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 |
|
224 | state.proxy.ws(req, socket, head, () => {
|
225 | socket.destroy();
|
226 | });
|
227 | },
|
228 | () => {
|
229 |
|
230 | socket.destroy();
|
231 | }
|
232 | );
|
233 | });
|
234 |
|
235 |
|
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;
|
248 | }
|
249 | };
|
250 |
|
251 | return this;
|
252 | };
|