UNPKG

9.4 kBJavaScriptView Raw
1const { spawn } = require('child_process');
2const http = require('http');
3const events = require('events');
4const webpack = require('webpack');
5const _ = require('lodash');
6const path = require('path');
7const webpackConfig = require(path.resolve(process.cwd(), './webpack.config'));
8const Logger = require('bunyan');
9const colors = require('colors');
10const chokidar = require('chokidar');
11
12const DATASTORE_PORT = 9000;
13
14const levelMap = {
15 info: colors.white,
16 error: colors.red,
17 warn: colors.yellow,
18};
19
20const componentColors = {
21 server: colors.blue,
22 webpack: colors.magenta,
23 datastore: colors.cyan,
24 dsui: colors.green,
25};
26
27class BunyanStream {
28 write(rec) {
29 const level = Logger.nameFromLevel[rec.level];
30 const time = rec.time instanceof Date ? rec.time.toISOString() : rec.time;
31 const formattedTime = `[${time.substr(11, 12)}]`.white;
32 const message = rec.msg.replace(/\n$/, '');
33
34 const levelColor = levelMap[level] || colors.white;
35 const component = rec.component || rec.name;
36 const service = rec.service ? `/${rec.service}` : '';
37 const componentColor = componentColors[component] || colors.white;
38 console.log(
39 `${formattedTime} ${componentColor(component + service)} ${levelColor(
40 level + ':',
41 )} ${message}`,
42 );
43 }
44}
45
46const { logger, loggingStream } = setupLogging();
47
48const { buildEvents, stopWebpack } = setupWebpack(logger);
49
50const { stopServer, startServer } = setupServer(
51 logger,
52 loggingStream,
53 buildEvents,
54);
55
56const { stopDatastore, startDatastore } = setupDatastore(logger, buildEvents);
57const { startDsui, stopDsui } = setupDsui(logger);
58
59buildEvents.once('reload', startServer);
60startDatastore();
61startDsui();
62
63// Handle process stop
64process.on('SIGINT', async function() {
65 logger.info('Caught interrupt signal');
66
67 await Promise.all([stopWebpack(), stopDatastore(), stopDsui(), stopServer()]);
68
69 logger.info('Shutting down');
70 process.exit();
71});
72
73// reload server on graphqls or config files change
74chokidar.watch(['./src/**/*.graphqls', './config/**/*.json'])
75 .on('all', (event, path) => {
76 if (event === 'add') {
77 console.log('added to watch list:', path);
78 } else if (event === 'change') {
79 console.log('file changed:', path);
80 stopServer()
81 .then(() => {
82 startServer();
83 })
84 }
85 });
86
87
88function setupLogging() {
89 const loggingStream = new BunyanStream();
90
91 // Logging
92 const logger = new Logger({
93 name: 'Development Server',
94 streams: [
95 {
96 level: 'info',
97 stream: loggingStream,
98 type: 'raw',
99 },
100 ],
101 });
102
103 return {
104 loggingStream,
105 logger,
106 };
107}
108
109function setupWebpack(logger) {
110 const webpackInstance = webpack(webpackConfig);
111
112 const eventEmitter = new events.EventEmitter();
113
114 const webpackLogger = logger.child({
115 component: 'webpack',
116 });
117
118 // Webpack
119 webpackLogger.info('Starting up compiler');
120 const webpackWatcher = webpackInstance.watch(
121 {
122 ignored: ['node_modules', 'dist'],
123 },
124 (err, stats) => {
125 if (err) {
126 webpackLogger.error('Error', err);
127 } else {
128 const info = stats.toJson();
129
130 if (stats.hasErrors()) {
131 info.errors.forEach(error => {
132 webpackLogger.error(error);
133 });
134 return;
135 }
136
137 if (stats.hasWarnings()) {
138 info.warning.forEach(warning => {
139 webpackLogger.warning(warning);
140 });
141 return;
142 }
143
144 webpackLogger.info('Rebuilt server - sending trigger to restart');
145 eventEmitter.emit('reload', { hash: stats.hash });
146 }
147 },
148 );
149
150 return {
151 buildEvents: eventEmitter,
152 stopWebpack: () => {
153 return new Promise(resolve => {
154 webpackLogger.trace('Killing webpack');
155
156 webpackWatcher.close(() => resolve());
157 });
158 },
159 };
160}
161
162function setupServer(logger, loggingStream, buildEvents) {
163 // Server
164 const serverLogger = logger.child({
165 component: 'server',
166 });
167 let child;
168 const startServer = () => {
169 child = spawn('node', ['./dist/server.js', '--inspect']);
170
171 child.stdout.on('data', data => {
172 const string = data.toString('utf8').split('\n');
173
174 string.forEach(line => {
175 if (line === '') {
176 return;
177 }
178
179 try {
180 const payload = JSON.parse(line);
181 loggingStream.write({
182 ...payload,
183 component: 'server',
184 });
185 } catch (ex) {
186 serverLogger.info(line);
187 }
188 });
189 });
190
191 child.stderr.on('data', data => {
192 const string = data.toString('utf8').split('\n');
193
194 string.forEach(line => {
195 if (line === '') {
196 return;
197 }
198
199 try {
200 const payload = JSON.parse(line);
201 loggingStream.write({
202 ...payload,
203 component: 'server',
204 });
205 } catch (ex) {
206 serverLogger.error(line);
207 }
208 });
209 });
210
211 const hotReload = () => {
212 child.kill('SIGUSR2');
213 };
214
215 buildEvents.on('reload', hotReload);
216
217 child.once('close', () => {
218 child = undefined;
219 serverLogger.warn(
220 'Server crashed waiting for restart trigger to restart',
221 );
222 buildEvents.removeListener('reload', hotReload);
223 buildEvents.once('reload', startServer);
224 });
225 };
226
227 return {
228 stopServer: () => {
229 return new Promise(resolve => {
230 if (child) {
231 serverLogger.trace('Killing server');
232
233 child.kill('SIGINT');
234
235 child.once('close', () => {
236 serverLogger.info('Server successfully shutdown');
237 resolve();
238 });
239 } else {
240 resolve();
241 }
242 });
243 },
244 startServer,
245 };
246}
247
248function setupDsui(logger) {
249 const dsuiLogger = logger.child({ component: 'dsui' });
250 let dsui;
251 const startDsui = () => {
252 // poll the status URL until the datastore emulator is ready
253 dsuiLogger.info('Waiting for Datastore emulator to start');
254 const timeout = setInterval(() => {
255 let body = '';
256 http.get(
257 {
258 hostname: 'localhost',
259 port: DATASTORE_PORT,
260 path: '/',
261 agent: false,
262 },
263 res => {
264 res.on('data', data => {
265 body += data;
266 });
267 res.on('end', () => {
268 if (res.statusCode == 200) {
269 dsuiLogger.info('Datastore emulator is ready, starting DSUI');
270 clearInterval(timeout);
271 const gcloud = spawn('gcloud', [
272 'beta',
273 'emulators',
274 'datastore',
275 'env-init',
276 '--format=json',
277 ]);
278 gcloud.stdout.on('data', data => {
279 const datastoreConfig = JSON.parse(data);
280 _.forEach(datastoreConfig, (value, key) => {
281 process.env[key] = value;
282 });
283 });
284
285 gcloud.once('close', () => {
286 dsui = spawn('npm', ['run', 'dsui']);
287 dsui.stdout.on('data', dsuiData => {
288 const string = dsuiData.toString('utf8');
289 dsuiLogger.info(string);
290 });
291
292 dsui.stderr.on('data', dsuiData => {
293 const string = dsuiData.toString('utf8');
294 dsuiLogger.error(string);
295 });
296 });
297 }
298 });
299 },
300 );
301 }, 2000);
302 };
303
304 const stopDsui = () => {
305 return new Promise(resolve => {
306 if (dsui) {
307 dsuiLogger.info('Killing DSUI');
308 dsui.kill('SIGINT');
309 dsui.once('close', () => {
310 logger.info('DSUI successfully shutdown');
311 resolve();
312 });
313 } else {
314 resolve();
315 }
316 });
317 };
318
319 return { startDsui, stopDsui };
320}
321
322function setupDatastore(logger, buildEvents) {
323 const datastoreLogger = logger.child({
324 component: 'datastore',
325 });
326 let datastore;
327 const startDatastore = () => {
328 datastoreLogger.info('Starting datastore emulator');
329 datastore = spawn('gcloud', [
330 'beta',
331 'emulators',
332 'datastore',
333 'start',
334 `--host-port=localhost:${DATASTORE_PORT}`,
335 ]);
336
337 datastore.stdout.on('data', data => {
338 let string = data.toString('utf8');
339 if (string.startsWith('[datastore] ')) {
340 string = string.substring(12);
341 }
342 if (string !== '\n') {
343 datastoreLogger.info(string);
344 }
345 });
346
347 datastore.stderr.on('data', data => {
348 let string = data.toString('utf8');
349 if (string.startsWith('[datastore] ')) {
350 string = string.substring(12);
351 }
352 if (string !== '\n') {
353 datastoreLogger.info(string);
354 }
355 });
356
357 datastore.once('close', () => {
358 datastore = undefined;
359 datastoreLogger.warn('Datastore crashed will restart after changes');
360 buildEvents.once('reload', startDatastore);
361 });
362 };
363
364 return {
365 stopDatastore: () => {
366 return new Promise(resolve => {
367 if (datastore) {
368 datastoreLogger.trace('Killing datastore');
369
370 datastore.kill('SIGINT');
371
372 datastore.once('close', () => {
373 logger.info('Datastore successfully shutdown');
374 resolve();
375 });
376 } else {
377 resolve();
378 }
379 });
380 },
381 startDatastore,
382 };
383}