UNPKG

4.18 kBJavaScriptView Raw
1import webpack from 'webpack';
2import { fork } from 'child_process';
3import EventEmitter from 'events';
4import Backoff from 'backo';
5import path from 'path';
6
7export default function createRenderServer(renderer, options) {
8 const serverCompiler = webpack(renderer);
9 const backoff = new Backoff({ min: 0, max: 1000 * 5 });
10 const events = new EventEmitter();
11 let child = null;
12 let addr = null;
13 let start = false;
14 let rip = false;
15 let port = 0;
16 let assetStats = null;
17 let serverStats = null;
18 const hot = true;
19
20 function send() {
21 if (child && assetStats) {
22 child.send({ type: 'stats', stats: assetStats });
23 }
24 }
25
26 function kill() {
27 if (child) {
28 let timeout = null;
29 child.once('exit', () => {
30 child = null;
31 if (timeout) {
32 clearTimeout(timeout);
33 }
34 });
35 child.kill('SIGINT');
36 timeout = setTimeout(() => {
37 child.kill('SIGTERM');
38 child = null;
39 timeout = null;
40 }, 3000);
41 }
42 }
43
44 function paths(base, entry) {
45 if (Array.isArray(entry)) {
46 return entry.map(value => paths(base, value));
47 } else if (typeof entry === 'string') {
48 return [ path.join(base, entry) ];
49 }
50 throw new TypeError('Invalid module path.');
51 }
52
53 function entry(module) {
54 const value = module.filter((entry) => /\.js$/.test(entry));
55 if (value.length > 0) {
56 return value[value.length - 1];
57 }
58 throw new TypeError('Module has no JavaScript files to run.');
59 }
60
61 function _spawn() {
62 // `assetsByChunkName` can map to either a string OR an array – since one
63 // chunk can have multiple assets (e.g. source maps).
64 const map = serverStats.toJson({ assets: true }).assetsByChunkName;
65 const modules = Object.keys(map).map(key => {
66 return paths(renderer.output.path, map[key]);
67 });
68 const env = {
69 ...process.env,
70 PORT: port,
71 HAS_WEBPACK_ASSET_EVENTS: 1,
72 HAS_WEBPACK_STATS_EVENTS: 1,
73 };
74
75 // Only support one entrypoint right now. Maybe support more later.
76 if (modules.length !== 1) {
77 throw new Error('Must only export 1 entrypoint!');
78 }
79 child = fork(entry(modules[0]), [ ], { env });
80 child.on('message', (message) => {
81 switch (message.type) {
82 case 'ping':
83 backoff.reset();
84 break;
85 case 'address':
86 addr = message.address;
87 events.emit('listening');
88 break;
89 default:
90 events.emit('error', new Error(`Unknown event: ${message.type}`));
91 }
92 });
93 child.once('exit', () => {
94 if (rip) {
95 events.emit('close');
96 } else {
97 spawn();
98 }
99 });
100 child.once('error', () => {
101 if (rip) {
102 events.emit('close');
103 } else {
104 spawn();
105 }
106 });
107 send();
108 }
109
110 function spawn() {
111 setTimeout(_spawn, backoff.duration());
112 }
113
114 function reload() {
115 if (hot) {
116 // If we're already running just invoke HMR, otherwise start up.
117 child.kill('SIGUSR2');
118 } else {
119 kill();
120 }
121 }
122
123 function trigger() {
124 if (!start || !serverStats) {
125 return;
126 } else if (child) {
127 reload();
128 } else {
129 spawn();
130 }
131 }
132
133 serverCompiler.watch({ }, (err, _stats) => {
134 // Bail on failure.
135 if (err) {
136 return;
137 }
138 serverStats = _stats;
139
140 /* eslint no-console: 0 */
141 console.log(serverStats.toString(options.stats));
142 trigger();
143 });
144
145 process.once('beforeExit', () => {
146 rip = true;
147 kill();
148 });
149
150 return Object.assign(events, {
151 compiler: serverCompiler,
152 close() {
153 rip = true;
154 kill();
155 },
156 listen(_port) {
157 start = true;
158 port = _port;
159 trigger();
160 },
161 address() {
162 return addr;
163 },
164 assets() {
165 send();
166 },
167 stats(_stats) {
168 assetStats = _stats.toJson({
169 hash: true,
170 version: false,
171 timings: false,
172 assets: true,
173 chunks: true,
174 chunkModules: false,
175 modules: false,
176 cached: false,
177 reasons: false,
178 source: false,
179 errorDetails: false,
180 chunkOrigins: false,
181 });
182 send();
183 },
184 });
185}