webpack-dev-server 3.1.6 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/bin/options.js +164 -0
- package/bin/utils.js +114 -0
- package/bin/webpack-dev-server.js +244 -328
- package/lib/Server.js +381 -208
- package/lib/options.json +354 -0
- package/lib/utils/addEntries.js +55 -0
- package/lib/utils/createCertificate.js +65 -0
- package/lib/utils/createDomain.js +35 -0
- package/lib/utils/createLogger.js +26 -0
- package/package.json +3 -2
- package/ssl/server.pem +39 -39
- package/lib/OptionsValidationError.js +0 -152
- package/lib/createLog.js +0 -19
- package/lib/optionsSchema.json +0 -350
- package/lib/util/addDevServerEntrypoints.js +0 -44
- package/lib/util/createDomain.js +0 -23
package/lib/Server.js
CHANGED
|
@@ -1,40 +1,58 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/* eslint
|
|
4
|
-
|
|
3
|
+
/* eslint-disable
|
|
4
|
+
import/order,
|
|
5
|
+
no-shadow,
|
|
6
|
+
no-undefined,
|
|
7
|
+
func-names,
|
|
8
|
+
multiline-ternary,
|
|
9
|
+
array-bracket-spacing,
|
|
10
|
+
space-before-function-paren
|
|
11
|
+
*/
|
|
5
12
|
const fs = require('fs');
|
|
6
|
-
const http = require('http');
|
|
7
13
|
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const ip = require('ip');
|
|
8
16
|
const url = require('url');
|
|
9
|
-
const
|
|
10
|
-
const
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const https = require('https');
|
|
19
|
+
const spdy = require('spdy');
|
|
20
|
+
const sockjs = require('sockjs');
|
|
21
|
+
|
|
22
|
+
const killable = require('killable');
|
|
23
|
+
|
|
11
24
|
const del = require('del');
|
|
25
|
+
const chokidar = require('chokidar');
|
|
26
|
+
|
|
12
27
|
const express = require('express');
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const killable = require('killable');
|
|
28
|
+
|
|
29
|
+
const compress = require('compression');
|
|
16
30
|
const serveIndex = require('serve-index');
|
|
31
|
+
const httpProxyMiddleware = require('http-proxy-middleware');
|
|
17
32
|
const historyApiFallback = require('connect-history-api-fallback');
|
|
18
|
-
|
|
19
|
-
const sockjs = require('sockjs');
|
|
20
|
-
const spdy = require('spdy');
|
|
33
|
+
|
|
21
34
|
const webpack = require('webpack');
|
|
22
35
|
const webpackDevMiddleware = require('webpack-dev-middleware');
|
|
23
|
-
const createLog = require('./createLog');
|
|
24
|
-
const OptionsValidationError = require('./OptionsValidationError');
|
|
25
|
-
const optionsSchema = require('./optionsSchema.json');
|
|
26
36
|
|
|
27
|
-
const
|
|
37
|
+
const createLogger = require('./utils/createLogger');
|
|
38
|
+
const createCertificate = require('./utils/createCertificate');
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!options) options = {};
|
|
32
|
-
this.log = _log || createLog(options);
|
|
40
|
+
const validateOptions = require('schema-utils');
|
|
41
|
+
const schema = require('./options.json');
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const STATS = {
|
|
44
|
+
all: false,
|
|
45
|
+
hash: true,
|
|
46
|
+
assets: true,
|
|
47
|
+
warnings: true,
|
|
48
|
+
errors: true,
|
|
49
|
+
errorDetails: false
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function Server (compiler, options = {}, _log) {
|
|
53
|
+
this.log = _log || createLogger(options);
|
|
54
|
+
|
|
55
|
+
validateOptions(schema, options, 'webpack Dev Server');
|
|
38
56
|
|
|
39
57
|
if (options.lazy && !options.filename) {
|
|
40
58
|
throw new Error("'filename' option must be set in lazy mode.");
|
|
@@ -42,49 +60,71 @@ function Server(compiler, options, _log) {
|
|
|
42
60
|
|
|
43
61
|
this.hot = options.hot || options.hotOnly;
|
|
44
62
|
this.headers = options.headers;
|
|
45
|
-
this.clientLogLevel = options.clientLogLevel;
|
|
46
|
-
this.clientOverlay = options.overlay;
|
|
47
63
|
this.progress = options.progress;
|
|
48
|
-
|
|
64
|
+
|
|
65
|
+
this.clientOverlay = options.overlay;
|
|
66
|
+
this.clientLogLevel = options.clientLogLevel;
|
|
67
|
+
|
|
49
68
|
this.publicHost = options.public;
|
|
50
69
|
this.allowedHosts = options.allowedHosts;
|
|
70
|
+
this.disableHostCheck = !!options.disableHostCheck;
|
|
71
|
+
|
|
51
72
|
this.sockets = [];
|
|
52
|
-
|
|
73
|
+
|
|
53
74
|
this.watchOptions = options.watchOptions || {};
|
|
75
|
+
this.contentBaseWatchers = [];
|
|
54
76
|
|
|
55
77
|
// Listening for events
|
|
56
78
|
const invalidPlugin = () => {
|
|
57
79
|
this.sockWrite(this.sockets, 'invalid');
|
|
58
80
|
};
|
|
81
|
+
|
|
59
82
|
if (this.progress) {
|
|
60
|
-
const progressPlugin = new webpack.ProgressPlugin(
|
|
61
|
-
percent
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
const progressPlugin = new webpack.ProgressPlugin(
|
|
84
|
+
(percent, msg, addInfo) => {
|
|
85
|
+
percent = Math.floor(percent * 100);
|
|
86
|
+
|
|
87
|
+
if (percent === 100) {
|
|
88
|
+
msg = 'Compilation completed';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (addInfo) {
|
|
92
|
+
msg = `${msg} (${addInfo})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.sockWrite(this.sockets, 'progress-update', { percent, msg });
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
66
99
|
progressPlugin.apply(compiler);
|
|
67
100
|
}
|
|
68
101
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
const addHooks = (compiler) => {
|
|
103
|
+
const { compile, invalid, done } = compiler.hooks;
|
|
104
|
+
|
|
105
|
+
compile.tap('webpack-dev-server', invalidPlugin);
|
|
106
|
+
invalid.tap('webpack-dev-server', invalidPlugin);
|
|
107
|
+
done.tap('webpack-dev-server', (stats) => {
|
|
108
|
+
this._sendStats(this.sockets, stats.toJson(STATS));
|
|
74
109
|
this._stats = stats;
|
|
75
110
|
});
|
|
76
111
|
};
|
|
112
|
+
|
|
77
113
|
if (compiler.compilers) {
|
|
78
|
-
compiler.compilers.forEach(
|
|
114
|
+
compiler.compilers.forEach(addHooks);
|
|
79
115
|
} else {
|
|
80
|
-
|
|
116
|
+
addHooks(compiler);
|
|
81
117
|
}
|
|
82
118
|
|
|
83
119
|
// Init express server
|
|
84
|
-
|
|
120
|
+
// eslint-disable-next-line
|
|
121
|
+
const app = this.app = new express();
|
|
122
|
+
|
|
123
|
+
app.all('*', (req, res, next) => {
|
|
124
|
+
if (this.checkHost(req.headers)) {
|
|
125
|
+
return next();
|
|
126
|
+
}
|
|
85
127
|
|
|
86
|
-
app.all('*', (req, res, next) => { // eslint-disable-line
|
|
87
|
-
if (this.checkHost(req.headers)) { return next(); }
|
|
88
128
|
res.send('Invalid Host header');
|
|
89
129
|
});
|
|
90
130
|
|
|
@@ -95,70 +135,106 @@ function Server(compiler, options, _log) {
|
|
|
95
135
|
|
|
96
136
|
app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
|
|
97
137
|
res.setHeader('Content-Type', 'application/javascript');
|
|
98
|
-
|
|
138
|
+
|
|
139
|
+
fs.createReadStream(
|
|
140
|
+
path.join(__dirname, '..', 'client', 'live.bundle.js')
|
|
141
|
+
).pipe(res);
|
|
99
142
|
});
|
|
100
143
|
|
|
101
144
|
app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
|
|
102
145
|
res.setHeader('Content-Type', 'application/javascript');
|
|
103
|
-
|
|
146
|
+
|
|
147
|
+
fs.createReadStream(
|
|
148
|
+
path.join(__dirname, '..', 'client', 'sockjs.bundle.js')
|
|
149
|
+
).pipe(res);
|
|
104
150
|
});
|
|
105
151
|
|
|
106
152
|
app.get('/webpack-dev-server.js', (req, res) => {
|
|
107
153
|
res.setHeader('Content-Type', 'application/javascript');
|
|
108
|
-
|
|
154
|
+
|
|
155
|
+
fs.createReadStream(
|
|
156
|
+
path.join(__dirname, '..', 'client', 'index.bundle.js')
|
|
157
|
+
).pipe(res);
|
|
109
158
|
});
|
|
110
159
|
|
|
111
160
|
app.get('/webpack-dev-server/*', (req, res) => {
|
|
112
161
|
res.setHeader('Content-Type', 'text/html');
|
|
113
|
-
|
|
162
|
+
|
|
163
|
+
fs.createReadStream(
|
|
164
|
+
path.join(__dirname, '..', 'client', 'live.html')
|
|
165
|
+
).pipe(res);
|
|
114
166
|
});
|
|
115
167
|
|
|
116
168
|
app.get('/webpack-dev-server', (req, res) => {
|
|
117
169
|
res.setHeader('Content-Type', 'text/html');
|
|
118
|
-
|
|
119
|
-
|
|
170
|
+
|
|
171
|
+
res.write(
|
|
172
|
+
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const outputPath = this.middleware.getFilenameFromUrl(
|
|
176
|
+
options.publicPath || '/'
|
|
177
|
+
);
|
|
178
|
+
|
|
120
179
|
const filesystem = this.middleware.fileSystem;
|
|
121
180
|
|
|
122
181
|
function writeDirectory(baseUrl, basePath) {
|
|
123
182
|
const content = filesystem.readdirSync(basePath);
|
|
183
|
+
|
|
124
184
|
res.write('<ul>');
|
|
185
|
+
|
|
125
186
|
content.forEach((item) => {
|
|
126
187
|
const p = `${basePath}/${item}`;
|
|
188
|
+
|
|
127
189
|
if (filesystem.statSync(p).isFile()) {
|
|
128
190
|
res.write('<li><a href="');
|
|
129
191
|
res.write(baseUrl + item);
|
|
130
192
|
res.write('">');
|
|
131
193
|
res.write(item);
|
|
132
194
|
res.write('</a></li>');
|
|
195
|
+
|
|
133
196
|
if (/\.js$/.test(item)) {
|
|
134
|
-
const
|
|
197
|
+
const html = item.substr(0, item.length - 3);
|
|
198
|
+
|
|
135
199
|
res.write('<li><a href="');
|
|
136
|
-
res.write(baseUrl +
|
|
200
|
+
res.write(baseUrl + html);
|
|
137
201
|
res.write('">');
|
|
138
|
-
res.write(
|
|
202
|
+
res.write(html);
|
|
139
203
|
res.write('</a> (magic html for ');
|
|
140
204
|
res.write(item);
|
|
141
205
|
res.write(') (<a href="');
|
|
142
|
-
res.write(
|
|
206
|
+
res.write(
|
|
207
|
+
baseUrl.replace(
|
|
208
|
+
// eslint-disable-next-line
|
|
209
|
+
/(^(https?:\/\/[^\/]+)?\/)/,
|
|
210
|
+
'$1webpack-dev-server/'
|
|
211
|
+
) + html
|
|
212
|
+
);
|
|
143
213
|
res.write('">webpack-dev-server</a>)</li>');
|
|
144
214
|
}
|
|
145
215
|
} else {
|
|
146
216
|
res.write('<li>');
|
|
147
217
|
res.write(item);
|
|
148
218
|
res.write('<br>');
|
|
219
|
+
|
|
149
220
|
writeDirectory(`${baseUrl + item}/`, p);
|
|
221
|
+
|
|
150
222
|
res.write('</li>');
|
|
151
223
|
}
|
|
152
224
|
});
|
|
225
|
+
|
|
153
226
|
res.write('</ul>');
|
|
154
227
|
}
|
|
228
|
+
|
|
155
229
|
writeDirectory(options.publicPath || '/', outputPath);
|
|
230
|
+
|
|
156
231
|
res.end('</body></html>');
|
|
157
232
|
});
|
|
158
233
|
|
|
159
234
|
let contentBase;
|
|
160
|
-
|
|
161
|
-
|
|
235
|
+
|
|
236
|
+
if (options.contentBase !== undefined) {
|
|
237
|
+
contentBase = options.contentBase;
|
|
162
238
|
} else {
|
|
163
239
|
contentBase = process.cwd();
|
|
164
240
|
}
|
|
@@ -167,14 +243,13 @@ function Server(compiler, options, _log) {
|
|
|
167
243
|
const websocketProxies = [];
|
|
168
244
|
|
|
169
245
|
const features = {
|
|
170
|
-
compress() {
|
|
246
|
+
compress: () => {
|
|
171
247
|
if (options.compress) {
|
|
172
248
|
// Enable gzip compression.
|
|
173
249
|
app.use(compress());
|
|
174
250
|
}
|
|
175
251
|
},
|
|
176
|
-
|
|
177
|
-
proxy() {
|
|
252
|
+
proxy: () => {
|
|
178
253
|
if (options.proxy) {
|
|
179
254
|
/**
|
|
180
255
|
* Assume a proxy configuration specified as:
|
|
@@ -190,7 +265,9 @@ function Server(compiler, options, _log) {
|
|
|
190
265
|
options.proxy = Object.keys(options.proxy).map((context) => {
|
|
191
266
|
let proxyOptions;
|
|
192
267
|
// For backwards compatibility reasons.
|
|
193
|
-
const correctedContext = context
|
|
268
|
+
const correctedContext = context
|
|
269
|
+
.replace(/^\*$/, '**')
|
|
270
|
+
.replace(/\/\*$/, '');
|
|
194
271
|
|
|
195
272
|
if (typeof options.proxy[context] === 'string') {
|
|
196
273
|
proxyOptions = {
|
|
@@ -201,6 +278,7 @@ function Server(compiler, options, _log) {
|
|
|
201
278
|
proxyOptions = Object.assign({}, options.proxy[context]);
|
|
202
279
|
proxyOptions.context = correctedContext;
|
|
203
280
|
}
|
|
281
|
+
|
|
204
282
|
proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
|
|
205
283
|
|
|
206
284
|
return proxyOptions;
|
|
@@ -209,14 +287,12 @@ function Server(compiler, options, _log) {
|
|
|
209
287
|
|
|
210
288
|
const getProxyMiddleware = (proxyConfig) => {
|
|
211
289
|
const context = proxyConfig.context || proxyConfig.path;
|
|
212
|
-
|
|
213
290
|
// It is possible to use the `bypass` method without a `target`.
|
|
214
291
|
// However, the proxy middleware has no use in this case, and will fail to instantiate.
|
|
215
292
|
if (proxyConfig.target) {
|
|
216
293
|
return httpProxyMiddleware(context, proxyConfig);
|
|
217
294
|
}
|
|
218
295
|
};
|
|
219
|
-
|
|
220
296
|
/**
|
|
221
297
|
* Assume a proxy configuration specified as:
|
|
222
298
|
* proxy: [
|
|
@@ -244,6 +320,7 @@ function Server(compiler, options, _log) {
|
|
|
244
320
|
}
|
|
245
321
|
|
|
246
322
|
proxyMiddleware = getProxyMiddleware(proxyConfig);
|
|
323
|
+
|
|
247
324
|
if (proxyConfig.ws) {
|
|
248
325
|
websocketProxies.push(proxyMiddleware);
|
|
249
326
|
}
|
|
@@ -251,17 +328,20 @@ function Server(compiler, options, _log) {
|
|
|
251
328
|
app.use((req, res, next) => {
|
|
252
329
|
if (typeof proxyConfigOrCallback === 'function') {
|
|
253
330
|
const newProxyConfig = proxyConfigOrCallback();
|
|
331
|
+
|
|
254
332
|
if (newProxyConfig !== proxyConfig) {
|
|
255
333
|
proxyConfig = newProxyConfig;
|
|
256
334
|
proxyMiddleware = getProxyMiddleware(proxyConfig);
|
|
257
335
|
}
|
|
258
336
|
}
|
|
337
|
+
|
|
259
338
|
const bypass = typeof proxyConfig.bypass === 'function';
|
|
260
|
-
|
|
261
|
-
const bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
|
|
339
|
+
|
|
340
|
+
const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;
|
|
262
341
|
|
|
263
342
|
if (bypassUrl) {
|
|
264
343
|
req.url = bypassUrl;
|
|
344
|
+
|
|
265
345
|
next();
|
|
266
346
|
} else if (proxyMiddleware) {
|
|
267
347
|
return proxyMiddleware(req, res, next);
|
|
@@ -272,37 +352,50 @@ function Server(compiler, options, _log) {
|
|
|
272
352
|
});
|
|
273
353
|
}
|
|
274
354
|
},
|
|
275
|
-
|
|
276
|
-
historyApiFallback() {
|
|
355
|
+
historyApiFallback: () => {
|
|
277
356
|
if (options.historyApiFallback) {
|
|
357
|
+
const fallback = typeof options.historyApiFallback === 'object'
|
|
358
|
+
? options.historyApiFallback
|
|
359
|
+
: null;
|
|
278
360
|
// Fall back to /index.html if nothing else matches.
|
|
279
|
-
app.use(historyApiFallback(
|
|
361
|
+
app.use(historyApiFallback(fallback));
|
|
280
362
|
}
|
|
281
363
|
},
|
|
282
|
-
|
|
283
364
|
contentBaseFiles: () => {
|
|
284
365
|
if (Array.isArray(contentBase)) {
|
|
285
366
|
contentBase.forEach((item) => {
|
|
286
367
|
app.get('*', express.static(item));
|
|
287
368
|
});
|
|
288
369
|
} else if (/^(https?:)?\/\//.test(contentBase)) {
|
|
289
|
-
this.log.warn(
|
|
290
|
-
|
|
370
|
+
this.log.warn(
|
|
371
|
+
'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
this.log.warn(
|
|
375
|
+
'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
|
|
376
|
+
);
|
|
291
377
|
// Redirect every request to contentBase
|
|
292
378
|
app.get('*', (req, res) => {
|
|
293
379
|
res.writeHead(302, {
|
|
294
380
|
Location: contentBase + req.path + (req._parsedUrl.search || '')
|
|
295
381
|
});
|
|
382
|
+
|
|
296
383
|
res.end();
|
|
297
384
|
});
|
|
298
385
|
} else if (typeof contentBase === 'number') {
|
|
299
|
-
this.log.warn(
|
|
300
|
-
|
|
386
|
+
this.log.warn(
|
|
387
|
+
'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
this.log.warn(
|
|
391
|
+
'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
|
|
392
|
+
);
|
|
301
393
|
// Redirect every request to the port contentBase
|
|
302
394
|
app.get('*', (req, res) => {
|
|
303
395
|
res.writeHead(302, {
|
|
304
396
|
Location: `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ''}`
|
|
305
397
|
});
|
|
398
|
+
|
|
306
399
|
res.end();
|
|
307
400
|
});
|
|
308
401
|
} else {
|
|
@@ -310,19 +403,23 @@ function Server(compiler, options, _log) {
|
|
|
310
403
|
app.get('*', express.static(contentBase, options.staticOptions));
|
|
311
404
|
}
|
|
312
405
|
},
|
|
313
|
-
|
|
314
|
-
contentBaseIndex() {
|
|
406
|
+
contentBaseIndex: () => {
|
|
315
407
|
if (Array.isArray(contentBase)) {
|
|
316
408
|
contentBase.forEach((item) => {
|
|
317
409
|
app.get('*', serveIndex(item));
|
|
318
410
|
});
|
|
319
|
-
} else if (
|
|
411
|
+
} else if (
|
|
412
|
+
!/^(https?:)?\/\//.test(contentBase) &&
|
|
413
|
+
typeof contentBase !== 'number'
|
|
414
|
+
) {
|
|
320
415
|
app.get('*', serveIndex(contentBase));
|
|
321
416
|
}
|
|
322
417
|
},
|
|
323
|
-
|
|
324
418
|
watchContentBase: () => {
|
|
325
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
/^(https?:)?\/\//.test(contentBase) ||
|
|
421
|
+
typeof contentBase === 'number'
|
|
422
|
+
) {
|
|
326
423
|
throw new Error('Watching remote files is not supported.');
|
|
327
424
|
} else if (Array.isArray(contentBase)) {
|
|
328
425
|
contentBase.forEach((item) => {
|
|
@@ -332,51 +429,78 @@ function Server(compiler, options, _log) {
|
|
|
332
429
|
this._watch(contentBase);
|
|
333
430
|
}
|
|
334
431
|
},
|
|
335
|
-
|
|
336
432
|
before: () => {
|
|
337
433
|
if (typeof options.before === 'function') {
|
|
338
434
|
options.before(app, this);
|
|
339
435
|
}
|
|
340
436
|
},
|
|
341
|
-
|
|
342
437
|
middleware: () => {
|
|
343
|
-
// include our middleware to ensure
|
|
438
|
+
// include our middleware to ensure
|
|
439
|
+
// it is able to handle '/index.html' request after redirect
|
|
344
440
|
app.use(this.middleware);
|
|
345
441
|
},
|
|
346
|
-
|
|
347
442
|
after: () => {
|
|
348
|
-
if (typeof options.after === 'function') {
|
|
443
|
+
if (typeof options.after === 'function') {
|
|
444
|
+
options.after(app, this);
|
|
445
|
+
}
|
|
349
446
|
},
|
|
350
|
-
|
|
351
447
|
headers: () => {
|
|
352
448
|
app.all('*', this.setContentHeaders.bind(this));
|
|
353
449
|
},
|
|
354
|
-
|
|
355
450
|
magicHtml: () => {
|
|
356
451
|
app.get('*', this.serveMagicHtml.bind(this));
|
|
357
452
|
},
|
|
358
|
-
|
|
359
453
|
setup: () => {
|
|
360
454
|
if (typeof options.setup === 'function') {
|
|
361
|
-
this.log.warn(
|
|
455
|
+
this.log.warn(
|
|
456
|
+
'The `setup` option is deprecated and will be removed in v3. Please update your config to use `before`'
|
|
457
|
+
);
|
|
458
|
+
|
|
362
459
|
options.setup(app, this);
|
|
363
460
|
}
|
|
364
461
|
}
|
|
365
462
|
};
|
|
366
463
|
|
|
367
|
-
const defaultFeatures = [
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
464
|
+
const defaultFeatures = [
|
|
465
|
+
'setup',
|
|
466
|
+
'before',
|
|
467
|
+
'headers',
|
|
468
|
+
'middleware'
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
if (options.proxy) {
|
|
472
|
+
defaultFeatures.push('proxy', 'middleware');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (contentBase !== false) {
|
|
476
|
+
defaultFeatures.push('contentBaseFiles');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (options.watchContentBase) {
|
|
480
|
+
defaultFeatures.push('watchContentBase');
|
|
481
|
+
}
|
|
482
|
+
|
|
371
483
|
if (options.historyApiFallback) {
|
|
372
484
|
defaultFeatures.push('historyApiFallback', 'middleware');
|
|
373
|
-
|
|
485
|
+
|
|
486
|
+
if (contentBase !== false) {
|
|
487
|
+
defaultFeatures.push('contentBaseFiles');
|
|
488
|
+
}
|
|
374
489
|
}
|
|
490
|
+
|
|
375
491
|
defaultFeatures.push('magicHtml');
|
|
376
|
-
|
|
492
|
+
|
|
493
|
+
if (contentBase !== false) {
|
|
494
|
+
defaultFeatures.push('contentBaseIndex');
|
|
495
|
+
}
|
|
377
496
|
// compress is placed last and uses unshift so that it will be the first middleware used
|
|
378
|
-
if (options.compress) {
|
|
379
|
-
|
|
497
|
+
if (options.compress) {
|
|
498
|
+
defaultFeatures.unshift('compress');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (options.after) {
|
|
502
|
+
defaultFeatures.push('after');
|
|
503
|
+
}
|
|
380
504
|
|
|
381
505
|
(options.features || defaultFeatures).forEach((feature) => {
|
|
382
506
|
features[feature]();
|
|
@@ -386,91 +510,56 @@ function Server(compiler, options, _log) {
|
|
|
386
510
|
// for keep supporting CLI parameters
|
|
387
511
|
if (typeof options.https === 'boolean') {
|
|
388
512
|
options.https = {
|
|
389
|
-
key: options.key,
|
|
390
|
-
cert: options.cert,
|
|
391
513
|
ca: options.ca,
|
|
392
514
|
pfx: options.pfx,
|
|
515
|
+
key: options.key,
|
|
516
|
+
cert: options.cert,
|
|
393
517
|
passphrase: options.pfxPassphrase,
|
|
394
518
|
requestCert: options.requestCert || false
|
|
395
519
|
};
|
|
396
520
|
}
|
|
397
521
|
|
|
398
522
|
let fakeCert;
|
|
523
|
+
|
|
399
524
|
if (!options.https.key || !options.https.cert) {
|
|
400
525
|
// Use a self-signed certificate if no certificate was configured.
|
|
401
526
|
// Cycle certs every 24 hours
|
|
402
527
|
const certPath = path.join(__dirname, '../ssl/server.pem');
|
|
528
|
+
|
|
403
529
|
let certExists = fs.existsSync(certPath);
|
|
404
530
|
|
|
405
531
|
if (certExists) {
|
|
406
|
-
const certStat = fs.statSync(certPath);
|
|
407
532
|
const certTtl = 1000 * 60 * 60 * 24;
|
|
533
|
+
const certStat = fs.statSync(certPath);
|
|
534
|
+
|
|
408
535
|
const now = new Date();
|
|
409
536
|
|
|
410
537
|
// cert is more than 30 days old, kill it with fire
|
|
411
538
|
if ((now - certStat.ctime) / certTtl > 30) {
|
|
412
539
|
this.log.info('SSL Certificate is more than 30 days old. Removing.');
|
|
540
|
+
|
|
413
541
|
del.sync([certPath], { force: true });
|
|
542
|
+
|
|
414
543
|
certExists = false;
|
|
415
544
|
}
|
|
416
545
|
}
|
|
417
546
|
|
|
418
547
|
if (!certExists) {
|
|
419
548
|
this.log.info('Generating SSL Certificate');
|
|
420
|
-
const attrs = [{ name: 'commonName', value: 'localhost' }];
|
|
421
|
-
const pems = selfsigned.generate(attrs, {
|
|
422
|
-
algorithm: 'sha256',
|
|
423
|
-
days: 30,
|
|
424
|
-
keySize: 2048,
|
|
425
|
-
extensions: [{
|
|
426
|
-
name: 'basicConstraints',
|
|
427
|
-
cA: true
|
|
428
|
-
}, {
|
|
429
|
-
name: 'keyUsage',
|
|
430
|
-
keyCertSign: true,
|
|
431
|
-
digitalSignature: true,
|
|
432
|
-
nonRepudiation: true,
|
|
433
|
-
keyEncipherment: true,
|
|
434
|
-
dataEncipherment: true
|
|
435
|
-
}, {
|
|
436
|
-
name: 'subjectAltName',
|
|
437
|
-
altNames: [
|
|
438
|
-
{
|
|
439
|
-
// type 2 is DNS
|
|
440
|
-
type: 2,
|
|
441
|
-
value: 'localhost'
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
type: 2,
|
|
445
|
-
value: 'localhost.localdomain'
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
type: 2,
|
|
449
|
-
value: 'lvh.me'
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
type: 2,
|
|
453
|
-
value: '*.lvh.me'
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
type: 2,
|
|
457
|
-
value: '[::1]'
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
// type 7 is IP
|
|
461
|
-
type: 7,
|
|
462
|
-
ip: '127.0.0.1'
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
type: 7,
|
|
466
|
-
ip: 'fe80::1'
|
|
467
|
-
}
|
|
468
|
-
]
|
|
469
|
-
}]
|
|
470
|
-
});
|
|
471
549
|
|
|
472
|
-
|
|
550
|
+
const attrs = [
|
|
551
|
+
{ name: 'commonName', value: 'localhost' }
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
const pems = createCertificate(attrs);
|
|
555
|
+
|
|
556
|
+
fs.writeFileSync(
|
|
557
|
+
certPath,
|
|
558
|
+
pems.private + pems.cert,
|
|
559
|
+
{ encoding: 'utf-8' }
|
|
560
|
+
);
|
|
473
561
|
}
|
|
562
|
+
|
|
474
563
|
fakeCert = fs.readFileSync(certPath);
|
|
475
564
|
}
|
|
476
565
|
|
|
@@ -483,7 +572,20 @@ function Server(compiler, options, _log) {
|
|
|
483
572
|
};
|
|
484
573
|
}
|
|
485
574
|
|
|
486
|
-
|
|
575
|
+
// `spdy` is effectively unmaintained, and as a consequence of an
|
|
576
|
+
// implementation that extensively relies on Node’s non-public APIs, broken
|
|
577
|
+
// on Node 10 and above. In those cases, only https will be used for now.
|
|
578
|
+
// Once express supports Node's built-in HTTP/2 support, migrating over to
|
|
579
|
+
// that should be the best way to go.
|
|
580
|
+
// The relevant issues are:
|
|
581
|
+
// - https://github.com/nodejs/node/issues/21665
|
|
582
|
+
// - https://github.com/webpack/webpack-dev-server/issues/1449
|
|
583
|
+
// - https://github.com/expressjs/express/issues/3388
|
|
584
|
+
if (+process.version.match(/^v(\d+)/)[1] >= 10) {
|
|
585
|
+
this.listeningApp = https.createServer(options.https, app);
|
|
586
|
+
} else {
|
|
587
|
+
this.listeningApp = spdy.createServer(options.https, app);
|
|
588
|
+
}
|
|
487
589
|
} else {
|
|
488
590
|
this.listeningApp = http.createServer(app);
|
|
489
591
|
}
|
|
@@ -514,50 +616,70 @@ Server.prototype.setContentHeaders = function (req, res, next) {
|
|
|
514
616
|
|
|
515
617
|
Server.prototype.checkHost = function (headers) {
|
|
516
618
|
// allow user to opt-out this security check, at own risk
|
|
517
|
-
if (this.disableHostCheck)
|
|
518
|
-
|
|
619
|
+
if (this.disableHostCheck) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
519
622
|
// get the Host header and extract hostname
|
|
520
623
|
// we don't care about port not matching
|
|
521
624
|
const hostHeader = headers.host;
|
|
522
|
-
|
|
625
|
+
|
|
626
|
+
if (!hostHeader) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
523
629
|
|
|
524
630
|
// use the node url-parser to retrieve the hostname from the host-header.
|
|
525
631
|
const hostname = url.parse(`//${hostHeader}`, false, true).hostname;
|
|
526
|
-
|
|
527
632
|
// always allow requests with explicit IPv4 or IPv6-address.
|
|
528
|
-
// A note on IPv6 addresses:
|
|
529
|
-
//
|
|
633
|
+
// A note on IPv6 addresses:
|
|
634
|
+
// hostHeader will always contain the brackets denoting
|
|
635
|
+
// an IPv6-address in URLs,
|
|
636
|
+
// these are removed from the hostname in url.parse(),
|
|
530
637
|
// so we have the pure IPv6-address in hostname.
|
|
531
|
-
if (ip.isV4Format(hostname) || ip.isV6Format(hostname))
|
|
532
|
-
|
|
638
|
+
if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) {
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
533
641
|
// always allow localhost host, for convience
|
|
534
|
-
if (hostname === 'localhost')
|
|
535
|
-
|
|
642
|
+
if (hostname === 'localhost') {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
536
645
|
// allow if hostname is in allowedHosts
|
|
537
646
|
if (this.allowedHosts && this.allowedHosts.length) {
|
|
538
647
|
for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
|
|
539
648
|
const allowedHost = this.allowedHosts[hostIdx];
|
|
649
|
+
|
|
540
650
|
if (allowedHost === hostname) return true;
|
|
541
651
|
|
|
542
652
|
// support "." as a subdomain wildcard
|
|
543
653
|
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
|
|
544
654
|
if (allowedHost[0] === '.') {
|
|
545
655
|
// "example.com"
|
|
546
|
-
if (hostname === allowedHost.substring(1))
|
|
656
|
+
if (hostname === allowedHost.substring(1)) {
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
547
659
|
// "*.example.com"
|
|
548
|
-
if (hostname.endsWith(allowedHost))
|
|
660
|
+
if (hostname.endsWith(allowedHost)) {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
549
663
|
}
|
|
550
664
|
}
|
|
551
665
|
}
|
|
552
666
|
|
|
553
667
|
// allow hostname of listening adress
|
|
554
|
-
if (hostname === this.
|
|
668
|
+
if (hostname === this.hostname) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
555
671
|
|
|
556
672
|
// also allow public hostname if provided
|
|
557
673
|
if (typeof this.publicHost === 'string') {
|
|
558
674
|
const idxPublic = this.publicHost.indexOf(':');
|
|
559
|
-
|
|
560
|
-
|
|
675
|
+
|
|
676
|
+
const publicHostname = idxPublic >= 0
|
|
677
|
+
? this.publicHost.substr(0, idxPublic)
|
|
678
|
+
: this.publicHost;
|
|
679
|
+
|
|
680
|
+
if (hostname === publicHostname) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
561
683
|
}
|
|
562
684
|
|
|
563
685
|
// disallow
|
|
@@ -566,9 +688,10 @@ Server.prototype.checkHost = function (headers) {
|
|
|
566
688
|
|
|
567
689
|
// delegate listen call and init sockjs
|
|
568
690
|
Server.prototype.listen = function (port, hostname, fn) {
|
|
569
|
-
this.
|
|
691
|
+
this.hostname = hostname;
|
|
692
|
+
|
|
570
693
|
const returnValue = this.listeningApp.listen(port, hostname, (err) => {
|
|
571
|
-
const
|
|
694
|
+
const socket = sockjs.createServer({
|
|
572
695
|
// Use provided up-to-date sockjs-client
|
|
573
696
|
sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
|
|
574
697
|
// Limit useless logs
|
|
@@ -581,35 +704,53 @@ Server.prototype.listen = function (port, hostname, fn) {
|
|
|
581
704
|
}
|
|
582
705
|
});
|
|
583
706
|
|
|
584
|
-
|
|
585
|
-
if (!
|
|
586
|
-
if (!this.checkHost(conn.headers)) {
|
|
587
|
-
this.sockWrite([conn], 'error', 'Invalid Host header');
|
|
588
|
-
conn.close();
|
|
707
|
+
socket.on('connection', (connection) => {
|
|
708
|
+
if (!connection) {
|
|
589
709
|
return;
|
|
590
710
|
}
|
|
591
|
-
this.sockets.push(conn);
|
|
592
711
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
712
|
+
if (!this.checkHost(connection.headers)) {
|
|
713
|
+
this.sockWrite([ connection ], 'error', 'Invalid Host header');
|
|
714
|
+
|
|
715
|
+
connection.close();
|
|
716
|
+
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this.sockets.push(connection);
|
|
721
|
+
|
|
722
|
+
connection.on('close', () => {
|
|
723
|
+
const idx = this.sockets.indexOf(connection);
|
|
724
|
+
|
|
725
|
+
if (idx >= 0) {
|
|
726
|
+
this.sockets.splice(idx, 1);
|
|
597
727
|
}
|
|
598
728
|
});
|
|
599
729
|
|
|
600
|
-
if (this.
|
|
730
|
+
if (this.hot) {
|
|
731
|
+
this.sockWrite([ connection ], 'hot');
|
|
732
|
+
}
|
|
601
733
|
|
|
602
|
-
if (this.progress) {
|
|
734
|
+
if (this.progress) {
|
|
735
|
+
this.sockWrite([ connection ], 'progress', this.progress);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (this.clientOverlay) {
|
|
739
|
+
this.sockWrite([ connection ], 'overlay', this.clientOverlay);
|
|
740
|
+
}
|
|
603
741
|
|
|
604
|
-
if (this.
|
|
742
|
+
if (this.clientLogLevel) {
|
|
743
|
+
this.sockWrite([ connection ], 'log-level', this.clientLogLevel);
|
|
744
|
+
}
|
|
605
745
|
|
|
606
|
-
if (this.
|
|
746
|
+
if (!this._stats) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
607
749
|
|
|
608
|
-
|
|
609
|
-
this._sendStats([conn], this._stats.toJson(clientStats), true);
|
|
750
|
+
this._sendStats([ connection ], this._stats.toJson(STATS), true);
|
|
610
751
|
});
|
|
611
752
|
|
|
612
|
-
|
|
753
|
+
socket.installHandlers(this.listeningApp, {
|
|
613
754
|
prefix: '/sockjs-node'
|
|
614
755
|
});
|
|
615
756
|
|
|
@@ -621,64 +762,89 @@ Server.prototype.listen = function (port, hostname, fn) {
|
|
|
621
762
|
return returnValue;
|
|
622
763
|
};
|
|
623
764
|
|
|
624
|
-
Server.prototype.close = function (
|
|
625
|
-
this.sockets.forEach((
|
|
626
|
-
|
|
765
|
+
Server.prototype.close = function (cb) {
|
|
766
|
+
this.sockets.forEach((socket) => {
|
|
767
|
+
socket.close();
|
|
627
768
|
});
|
|
769
|
+
|
|
628
770
|
this.sockets = [];
|
|
629
771
|
|
|
630
772
|
this.contentBaseWatchers.forEach((watcher) => {
|
|
631
773
|
watcher.close();
|
|
632
774
|
});
|
|
775
|
+
|
|
633
776
|
this.contentBaseWatchers = [];
|
|
634
777
|
|
|
635
778
|
this.listeningApp.kill(() => {
|
|
636
|
-
this.middleware.close(
|
|
779
|
+
this.middleware.close(cb);
|
|
637
780
|
});
|
|
638
781
|
};
|
|
639
782
|
|
|
640
783
|
Server.prototype.sockWrite = function (sockets, type, data) {
|
|
641
|
-
sockets.forEach((
|
|
642
|
-
|
|
643
|
-
type,
|
|
644
|
-
|
|
645
|
-
}));
|
|
784
|
+
sockets.forEach((socket) => {
|
|
785
|
+
socket.write(
|
|
786
|
+
JSON.stringify({ type, data })
|
|
787
|
+
);
|
|
646
788
|
});
|
|
647
789
|
};
|
|
648
790
|
|
|
649
791
|
Server.prototype.serveMagicHtml = function (req, res, next) {
|
|
650
792
|
const _path = req.path;
|
|
793
|
+
|
|
651
794
|
try {
|
|
652
|
-
|
|
795
|
+
const isFile = this.middleware.fileSystem.statSync(
|
|
796
|
+
this.middleware.getFilenameFromUrl(`${_path}.js`)
|
|
797
|
+
).isFile();
|
|
798
|
+
|
|
799
|
+
if (!isFile) {
|
|
800
|
+
return next();
|
|
801
|
+
}
|
|
653
802
|
// Serve a page that executes the javascript
|
|
654
|
-
res.write(
|
|
803
|
+
res.write(
|
|
804
|
+
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="'
|
|
805
|
+
);
|
|
655
806
|
res.write(_path);
|
|
656
807
|
res.write('.js');
|
|
657
808
|
res.write(req._parsedUrl.search || '');
|
|
809
|
+
|
|
658
810
|
res.end('"></script></body></html>');
|
|
659
|
-
} catch (
|
|
811
|
+
} catch (err) {
|
|
660
812
|
return next();
|
|
661
813
|
}
|
|
662
814
|
};
|
|
663
815
|
|
|
664
816
|
// send stats to a socket or multiple sockets
|
|
665
817
|
Server.prototype._sendStats = function (sockets, stats, force) {
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
818
|
+
if (
|
|
819
|
+
!force &&
|
|
820
|
+
stats &&
|
|
821
|
+
(!stats.errors || stats.errors.length === 0) &&
|
|
822
|
+
stats.assets &&
|
|
823
|
+
stats.assets.every(asset => !asset.emitted)
|
|
824
|
+
) {
|
|
825
|
+
return this.sockWrite(sockets, 'still-ok');
|
|
826
|
+
}
|
|
827
|
+
|
|
672
828
|
this.sockWrite(sockets, 'hash', stats.hash);
|
|
673
|
-
|
|
829
|
+
|
|
830
|
+
if (stats.errors.length > 0) {
|
|
831
|
+
this.sockWrite(sockets, 'errors', stats.errors);
|
|
832
|
+
} else if (stats.warnings.length > 0) {
|
|
833
|
+
this.sockWrite(sockets, 'warnings', stats.warnings);
|
|
834
|
+
} else {
|
|
835
|
+
this.sockWrite(sockets, 'ok');
|
|
836
|
+
}
|
|
674
837
|
};
|
|
675
838
|
|
|
676
839
|
Server.prototype._watch = function (watchPath) {
|
|
677
840
|
// duplicate the same massaging of options that watchpack performs
|
|
678
841
|
// https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
|
|
679
842
|
// this isn't an elegant solution, but we'll improve it in the future
|
|
680
|
-
const usePolling = this.watchOptions.poll ? true : undefined;
|
|
681
|
-
const interval = typeof this.watchOptions.poll === 'number'
|
|
843
|
+
const usePolling = this.watchOptions.poll ? true : undefined;
|
|
844
|
+
const interval = typeof this.watchOptions.poll === 'number'
|
|
845
|
+
? this.watchOptions.poll
|
|
846
|
+
: undefined;
|
|
847
|
+
|
|
682
848
|
const options = {
|
|
683
849
|
ignoreInitial: true,
|
|
684
850
|
persistent: true,
|
|
@@ -691,7 +857,10 @@ Server.prototype._watch = function (watchPath) {
|
|
|
691
857
|
usePolling,
|
|
692
858
|
interval
|
|
693
859
|
};
|
|
694
|
-
|
|
860
|
+
|
|
861
|
+
const watcher = chokidar.watch(watchPath, options);
|
|
862
|
+
|
|
863
|
+
watcher.on('change', () => {
|
|
695
864
|
this.sockWrite(this.sockets, 'content-changed');
|
|
696
865
|
});
|
|
697
866
|
|
|
@@ -699,10 +868,14 @@ Server.prototype._watch = function (watchPath) {
|
|
|
699
868
|
};
|
|
700
869
|
|
|
701
870
|
Server.prototype.invalidate = function () {
|
|
702
|
-
if (this.middleware)
|
|
871
|
+
if (this.middleware) {
|
|
872
|
+
this.middleware.invalidate();
|
|
873
|
+
}
|
|
703
874
|
};
|
|
704
875
|
|
|
705
|
-
// Export this logic,
|
|
706
|
-
|
|
876
|
+
// Export this logic,
|
|
877
|
+
// so that other implementations,
|
|
878
|
+
// like task-runners can use it
|
|
879
|
+
Server.addDevServerEntrypoints = require('./utils/addEntries');
|
|
707
880
|
|
|
708
881
|
module.exports = Server;
|