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