webpack-dev-server 3.3.1 → 3.5.1

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/lib/Server.js CHANGED
@@ -1,153 +1,169 @@
1
1
  'use strict';
2
2
 
3
3
  /* eslint-disable
4
- import/order,
5
4
  no-shadow,
6
5
  no-undefined,
7
6
  func-names
8
7
  */
9
8
  const fs = require('fs');
10
9
  const path = require('path');
11
-
12
- const ip = require('ip');
13
10
  const tls = require('tls');
14
11
  const url = require('url');
15
12
  const http = require('http');
16
13
  const https = require('https');
17
- const sockjs = require('sockjs');
18
-
14
+ const ip = require('ip');
19
15
  const semver = require('semver');
20
-
21
16
  const killable = require('killable');
22
-
23
- const del = require('del');
24
17
  const chokidar = require('chokidar');
25
-
26
18
  const express = require('express');
27
-
28
- const compress = require('compression');
29
- const serveIndex = require('serve-index');
30
19
  const httpProxyMiddleware = require('http-proxy-middleware');
31
20
  const historyApiFallback = require('connect-history-api-fallback');
32
-
21
+ const compress = require('compression');
22
+ const serveIndex = require('serve-index');
33
23
  const webpack = require('webpack');
34
24
  const webpackDevMiddleware = require('webpack-dev-middleware');
35
-
25
+ const validateOptions = require('schema-utils');
36
26
  const updateCompiler = require('./utils/updateCompiler');
37
27
  const createLogger = require('./utils/createLogger');
38
- const createCertificate = require('./utils/createCertificate');
39
-
40
- const validateOptions = require('schema-utils');
28
+ const getCertificate = require('./utils/getCertificate');
29
+ const status = require('./utils/status');
30
+ const createDomain = require('./utils/createDomain');
31
+ const runBonjour = require('./utils/runBonjour');
32
+ const routes = require('./utils/routes');
41
33
  const schema = require('./options.json');
42
-
43
- // Workaround for sockjs@~0.3.19
44
- // sockjs will remove Origin header, however Origin header is required for checking host.
45
- // See https://github.com/webpack/webpack-dev-server/issues/1604 for more information
46
- {
47
- // eslint-disable-next-line global-require
48
- const SockjsSession = require('sockjs/lib/transport').Session;
49
- const decorateConnection = SockjsSession.prototype.decorateConnection;
50
- SockjsSession.prototype.decorateConnection = function(req) {
51
- decorateConnection.call(this, req);
52
- const connection = this.connection;
53
- if (
54
- connection.headers &&
55
- !('origin' in connection.headers) &&
56
- 'origin' in req.headers
57
- ) {
58
- connection.headers.origin = req.headers.origin;
59
- }
60
- };
61
- }
34
+ const SockJSServer = require('./servers/SockJSServer');
62
35
 
63
36
  // Workaround for node ^8.6.0, ^9.0.0
64
37
  // DEFAULT_ECDH_CURVE is default to prime256v1 in these version
65
38
  // breaking connection when certificate is not signed with prime256v1
66
39
  // change it to auto allows OpenSSL to select the curve automatically
67
- // See https://github.com/nodejs/node/issues/16196 for more infomation
40
+ // See https://github.com/nodejs/node/issues/16196 for more information
68
41
  if (semver.satisfies(process.version, '8.6.0 - 9')) {
69
42
  tls.DEFAULT_ECDH_CURVE = 'auto';
70
43
  }
71
44
 
72
- class Server {
73
- static get DEFAULT_STATS() {
74
- return {
75
- all: false,
76
- hash: true,
77
- assets: true,
78
- warnings: true,
79
- errors: true,
80
- errorDetails: false,
81
- };
82
- }
45
+ if (!process.env.WEBPACK_DEV_SERVER) {
46
+ process.env.WEBPACK_DEV_SERVER = true;
47
+ }
83
48
 
49
+ class Server {
84
50
  constructor(compiler, options = {}, _log) {
85
- this.log = _log || createLogger(options);
86
-
87
- validateOptions(schema, options, 'webpack Dev Server');
88
-
89
51
  if (options.lazy && !options.filename) {
90
52
  throw new Error("'filename' option must be set in lazy mode.");
91
53
  }
92
54
 
93
- // if the user enables http2, we can safely enable https
94
- if (options.http2 && !options.https) {
95
- options.https = true;
96
- }
55
+ validateOptions(schema, options, 'webpack Dev Server');
97
56
 
98
57
  updateCompiler(compiler, options);
99
58
 
59
+ this.compiler = compiler;
60
+ this.options = options;
61
+
62
+ // Setup default value
63
+ this.options.contentBase =
64
+ this.options.contentBase !== undefined
65
+ ? this.options.contentBase
66
+ : process.cwd();
67
+
68
+ this.log = _log || createLogger(options);
69
+
100
70
  this.originalStats =
101
- options.stats && Object.keys(options.stats).length ? options.stats : {};
71
+ this.options.stats && Object.keys(this.options.stats).length
72
+ ? this.options.stats
73
+ : {};
102
74
 
103
- this.hot = options.hot || options.hotOnly;
104
- this.headers = options.headers;
105
- this.progress = options.progress;
75
+ this.sockets = [];
76
+ this.contentBaseWatchers = [];
106
77
 
107
- this.serveIndex = options.serveIndex;
78
+ // TODO this.<property> is deprecated (remove them in next major release.) in favor this.options.<property>
79
+ this.hot = this.options.hot || this.options.hotOnly;
80
+ this.headers = this.options.headers;
81
+ this.progress = this.options.progress;
108
82
 
109
- this.clientOverlay = options.overlay;
110
- this.clientLogLevel = options.clientLogLevel;
83
+ this.serveIndex = this.options.serveIndex;
111
84
 
112
- this.publicHost = options.public;
113
- this.allowedHosts = options.allowedHosts;
114
- this.disableHostCheck = !!options.disableHostCheck;
85
+ this.clientOverlay = this.options.overlay;
86
+ this.clientLogLevel = this.options.clientLogLevel;
115
87
 
116
- this.sockets = [];
88
+ this.publicHost = this.options.public;
89
+ this.allowedHosts = this.options.allowedHosts;
90
+ this.disableHostCheck = !!this.options.disableHostCheck;
91
+
92
+ if (!this.options.watchOptions) {
93
+ this.options.watchOptions = {};
94
+ }
95
+ // Ignoring node_modules folder by default
96
+ this.options.watchOptions.ignored = this.options.watchOptions.ignored || [
97
+ /node_modules/,
98
+ ];
99
+ this.watchOptions = this.options.watchOptions;
117
100
 
118
- this.watchOptions = options.watchOptions || {};
119
- this.contentBaseWatchers = [];
120
101
  // Replace leading and trailing slashes to normalize path
121
102
  this.sockPath = `/${
122
- options.sockPath
123
- ? options.sockPath.replace(/^\/|\/$/g, '')
103
+ this.options.sockPath
104
+ ? this.options.sockPath.replace(/^\/|\/$/g, '')
124
105
  : 'sockjs-node'
125
106
  }`;
126
107
 
127
- // Listening for events
128
- const invalidPlugin = () => {
129
- this.sockWrite(this.sockets, 'invalid');
130
- };
131
-
132
108
  if (this.progress) {
133
- const progressPlugin = new webpack.ProgressPlugin(
134
- (percent, msg, addInfo) => {
135
- percent = Math.floor(percent * 100);
109
+ this.setupProgressPlugin();
110
+ }
136
111
 
137
- if (percent === 100) {
138
- msg = 'Compilation completed';
139
- }
112
+ this.setupHooks();
113
+ this.setupApp();
114
+ this.setupCheckHostRoute();
115
+ this.setupDevMiddleware();
140
116
 
141
- if (addInfo) {
142
- msg = `${msg} (${addInfo})`;
143
- }
117
+ // set express routes
118
+ routes(this.app, this.middleware, this.options);
119
+
120
+ // Keep track of websocket proxies for external websocket upgrade.
121
+ this.websocketProxies = [];
122
+
123
+ this.setupFeatures();
124
+ this.setupHttps();
125
+ this.createServer();
144
126
 
145
- this.sockWrite(this.sockets, 'progress-update', { percent, msg });
127
+ killable(this.listeningApp);
128
+
129
+ // Proxy websockets without the initial http request
130
+ // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
131
+ this.websocketProxies.forEach(function(wsProxy) {
132
+ this.listeningApp.on('upgrade', wsProxy.upgrade);
133
+ }, this);
134
+ }
135
+
136
+ setupProgressPlugin() {
137
+ const progressPlugin = new webpack.ProgressPlugin(
138
+ (percent, msg, addInfo) => {
139
+ percent = Math.floor(percent * 100);
140
+
141
+ if (percent === 100) {
142
+ msg = 'Compilation completed';
146
143
  }
147
- );
148
144
 
149
- progressPlugin.apply(compiler);
150
- }
145
+ if (addInfo) {
146
+ msg = `${msg} (${addInfo})`;
147
+ }
148
+
149
+ this.sockWrite(this.sockets, 'progress-update', { percent, msg });
150
+ }
151
+ );
152
+
153
+ progressPlugin.apply(this.compiler);
154
+ }
155
+
156
+ setupApp() {
157
+ // Init express server
158
+ // eslint-disable-next-line new-cap
159
+ this.app = new express();
160
+ }
161
+
162
+ setupHooks() {
163
+ // Listening for events
164
+ const invalidPlugin = () => {
165
+ this.sockWrite(this.sockets, 'invalid');
166
+ };
151
167
 
152
168
  const addHooks = (compiler) => {
153
169
  const { compile, invalid, done } = compiler.hooks;
@@ -160,527 +176,450 @@ class Server {
160
176
  });
161
177
  };
162
178
 
163
- if (compiler.compilers) {
164
- compiler.compilers.forEach(addHooks);
179
+ if (this.compiler.compilers) {
180
+ this.compiler.compilers.forEach(addHooks);
165
181
  } else {
166
- addHooks(compiler);
167
- }
168
-
169
- // Init express server
170
- // eslint-disable-next-line
171
- const app = (this.app = new express());
172
-
173
- // ref: https://github.com/webpack/webpack-dev-server/issues/1575
174
- // ref: https://github.com/webpack/webpack-dev-server/issues/1724
175
- // remove this when send@^0.16.3
176
- if (express.static && express.static.mime && express.static.mime.types) {
177
- express.static.mime.types.wasm = 'application/wasm';
182
+ addHooks(this.compiler);
178
183
  }
184
+ }
179
185
 
180
- app.all('*', (req, res, next) => {
186
+ setupCheckHostRoute() {
187
+ this.app.all('*', (req, res, next) => {
181
188
  if (this.checkHost(req.headers)) {
182
189
  return next();
183
190
  }
184
191
 
185
192
  res.send('Invalid Host header');
186
193
  });
194
+ }
187
195
 
188
- const wdmOptions = { logLevel: this.log.options.level };
189
-
196
+ setupDevMiddleware() {
190
197
  // middleware for serving webpack bundle
191
198
  this.middleware = webpackDevMiddleware(
192
- compiler,
193
- Object.assign({}, options, wdmOptions)
199
+ this.compiler,
200
+ Object.assign({}, this.options, { logLevel: this.log.options.level })
194
201
  );
202
+ }
195
203
 
196
- app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
197
- res.setHeader('Content-Type', 'application/javascript');
204
+ setupCompressFeature() {
205
+ this.app.use(compress());
206
+ }
198
207
 
199
- fs.createReadStream(
200
- path.join(__dirname, '..', 'client', 'live.bundle.js')
201
- ).pipe(res);
202
- });
208
+ setupProxyFeature() {
209
+ /**
210
+ * Assume a proxy configuration specified as:
211
+ * proxy: {
212
+ * 'context': { options }
213
+ * }
214
+ * OR
215
+ * proxy: {
216
+ * 'context': 'target'
217
+ * }
218
+ */
219
+ if (!Array.isArray(this.options.proxy)) {
220
+ if (Object.prototype.hasOwnProperty.call(this.options.proxy, 'target')) {
221
+ this.options.proxy = [this.options.proxy];
222
+ } else {
223
+ this.options.proxy = Object.keys(this.options.proxy).map((context) => {
224
+ let proxyOptions;
225
+ // For backwards compatibility reasons.
226
+ const correctedContext = context
227
+ .replace(/^\*$/, '**')
228
+ .replace(/\/\*$/, '');
229
+
230
+ if (typeof this.options.proxy[context] === 'string') {
231
+ proxyOptions = {
232
+ context: correctedContext,
233
+ target: this.options.proxy[context],
234
+ };
235
+ } else {
236
+ proxyOptions = Object.assign({}, this.options.proxy[context]);
237
+ proxyOptions.context = correctedContext;
238
+ }
203
239
 
204
- app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
205
- res.setHeader('Content-Type', 'application/javascript');
240
+ proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
206
241
 
207
- fs.createReadStream(
208
- path.join(__dirname, '..', 'client', 'sockjs.bundle.js')
209
- ).pipe(res);
210
- });
242
+ return proxyOptions;
243
+ });
244
+ }
245
+ }
211
246
 
212
- app.get('/webpack-dev-server.js', (req, res) => {
213
- res.setHeader('Content-Type', 'application/javascript');
247
+ const getProxyMiddleware = (proxyConfig) => {
248
+ const context = proxyConfig.context || proxyConfig.path;
214
249
 
215
- fs.createReadStream(
216
- path.join(__dirname, '..', 'client', 'index.bundle.js')
217
- ).pipe(res);
218
- });
250
+ // It is possible to use the `bypass` method without a `target`.
251
+ // However, the proxy middleware has no use in this case, and will fail to instantiate.
252
+ if (proxyConfig.target) {
253
+ return httpProxyMiddleware(context, proxyConfig);
254
+ }
255
+ };
256
+ /**
257
+ * Assume a proxy configuration specified as:
258
+ * proxy: [
259
+ * {
260
+ * context: ...,
261
+ * ...options...
262
+ * },
263
+ * // or:
264
+ * function() {
265
+ * return {
266
+ * context: ...,
267
+ * ...options...
268
+ * };
269
+ * }
270
+ * ]
271
+ */
272
+ this.options.proxy.forEach((proxyConfigOrCallback) => {
273
+ let proxyConfig;
274
+ let proxyMiddleware;
275
+
276
+ if (typeof proxyConfigOrCallback === 'function') {
277
+ proxyConfig = proxyConfigOrCallback();
278
+ } else {
279
+ proxyConfig = proxyConfigOrCallback;
280
+ }
219
281
 
220
- app.get('/webpack-dev-server/*', (req, res) => {
221
- res.setHeader('Content-Type', 'text/html');
282
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
283
+
284
+ if (proxyConfig.ws) {
285
+ this.websocketProxies.push(proxyMiddleware);
286
+ }
222
287
 
223
- fs.createReadStream(
224
- path.join(__dirname, '..', 'client', 'live.html')
225
- ).pipe(res);
288
+ this.app.use((req, res, next) => {
289
+ if (typeof proxyConfigOrCallback === 'function') {
290
+ const newProxyConfig = proxyConfigOrCallback();
291
+
292
+ if (newProxyConfig !== proxyConfig) {
293
+ proxyConfig = newProxyConfig;
294
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
295
+ }
296
+ }
297
+
298
+ // - Check if we have a bypass function defined
299
+ // - In case the bypass function is defined we'll retrieve the
300
+ // bypassUrl from it otherwise byPassUrl would be null
301
+ const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
302
+ const bypassUrl = isByPassFuncDefined
303
+ ? proxyConfig.bypass(req, res, proxyConfig)
304
+ : null;
305
+
306
+ if (typeof bypassUrl === 'boolean') {
307
+ // skip the proxy
308
+ req.url = null;
309
+ next();
310
+ } else if (typeof bypassUrl === 'string') {
311
+ // byPass to that url
312
+ req.url = bypassUrl;
313
+ next();
314
+ } else if (proxyMiddleware) {
315
+ return proxyMiddleware(req, res, next);
316
+ } else {
317
+ next();
318
+ }
319
+ });
226
320
  });
321
+ }
227
322
 
228
- app.get('/webpack-dev-server', (req, res) => {
229
- res.setHeader('Content-Type', 'text/html');
323
+ setupHistoryApiFallbackFeature() {
324
+ const fallback =
325
+ typeof this.options.historyApiFallback === 'object'
326
+ ? this.options.historyApiFallback
327
+ : null;
230
328
 
231
- res.write(
232
- '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
329
+ // Fall back to /index.html if nothing else matches.
330
+ this.app.use(historyApiFallback(fallback));
331
+ }
332
+
333
+ setupStaticFeature() {
334
+ const contentBase = this.options.contentBase;
335
+
336
+ if (Array.isArray(contentBase)) {
337
+ contentBase.forEach((item) => {
338
+ this.app.get('*', express.static(item));
339
+ });
340
+ } else if (/^(https?:)?\/\//.test(contentBase)) {
341
+ this.log.warn(
342
+ 'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
233
343
  );
234
344
 
235
- const outputPath = this.middleware.getFilenameFromUrl(
236
- options.publicPath || '/'
345
+ this.log.warn(
346
+ 'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
237
347
  );
238
348
 
239
- const filesystem = this.middleware.fileSystem;
240
-
241
- function writeDirectory(baseUrl, basePath) {
242
- const content = filesystem.readdirSync(basePath);
243
-
244
- res.write('<ul>');
245
-
246
- content.forEach((item) => {
247
- const p = `${basePath}/${item}`;
248
-
249
- if (filesystem.statSync(p).isFile()) {
250
- res.write('<li><a href="');
251
- res.write(baseUrl + item);
252
- res.write('">');
253
- res.write(item);
254
- res.write('</a></li>');
255
-
256
- if (/\.js$/.test(item)) {
257
- const html = item.substr(0, item.length - 3);
258
-
259
- res.write('<li><a href="');
260
- res.write(baseUrl + html);
261
- res.write('">');
262
- res.write(html);
263
- res.write('</a> (magic html for ');
264
- res.write(item);
265
- res.write(') (<a href="');
266
- res.write(
267
- baseUrl.replace(
268
- // eslint-disable-next-line
269
- /(^(https?:\/\/[^\/]+)?\/)/,
270
- '$1webpack-dev-server/'
271
- ) + html
272
- );
273
- res.write('">webpack-dev-server</a>)</li>');
274
- }
275
- } else {
276
- res.write('<li>');
277
- res.write(item);
278
- res.write('<br>');
349
+ // Redirect every request to contentBase
350
+ this.app.get('*', (req, res) => {
351
+ res.writeHead(302, {
352
+ Location: contentBase + req.path + (req._parsedUrl.search || ''),
353
+ });
279
354
 
280
- writeDirectory(`${baseUrl + item}/`, p);
355
+ res.end();
356
+ });
357
+ } else if (typeof contentBase === 'number') {
358
+ this.log.warn(
359
+ 'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
360
+ );
281
361
 
282
- res.write('</li>');
283
- }
362
+ this.log.warn(
363
+ 'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
364
+ );
365
+
366
+ // Redirect every request to the port contentBase
367
+ this.app.get('*', (req, res) => {
368
+ res.writeHead(302, {
369
+ Location: `//localhost:${contentBase}${req.path}${req._parsedUrl
370
+ .search || ''}`,
284
371
  });
285
372
 
286
- res.write('</ul>');
287
- }
373
+ res.end();
374
+ });
375
+ } else {
376
+ // route content request
377
+ this.app.get(
378
+ '*',
379
+ express.static(contentBase, this.options.staticOptions)
380
+ );
381
+ }
382
+ }
288
383
 
289
- writeDirectory(options.publicPath || '/', outputPath);
384
+ setupServeIndexFeature() {
385
+ const contentBase = this.options.contentBase;
290
386
 
291
- res.end('</body></html>');
292
- });
387
+ if (Array.isArray(contentBase)) {
388
+ contentBase.forEach((item) => {
389
+ this.app.get('*', serveIndex(item));
390
+ });
391
+ } else if (
392
+ !/^(https?:)?\/\//.test(contentBase) &&
393
+ typeof contentBase !== 'number'
394
+ ) {
395
+ this.app.get('*', serveIndex(contentBase));
396
+ }
397
+ }
293
398
 
294
- let contentBase;
399
+ setupWatchStaticFeature() {
400
+ const contentBase = this.options.contentBase;
295
401
 
296
- if (options.contentBase !== undefined) {
297
- contentBase = options.contentBase;
402
+ if (
403
+ /^(https?:)?\/\//.test(contentBase) ||
404
+ typeof contentBase === 'number'
405
+ ) {
406
+ throw new Error('Watching remote files is not supported.');
407
+ } else if (Array.isArray(contentBase)) {
408
+ contentBase.forEach((item) => {
409
+ this._watch(item);
410
+ });
298
411
  } else {
299
- contentBase = process.cwd();
412
+ this._watch(contentBase);
300
413
  }
414
+ }
301
415
 
302
- // Keep track of websocket proxies for external websocket upgrade.
303
- const websocketProxies = [];
416
+ setupBeforeFeature() {
417
+ // Todo rename onBeforeSetupMiddleware in next major release
418
+ // Todo pass only `this` argument
419
+ this.options.before(this.app, this, this.compiler);
420
+ }
421
+
422
+ setupMiddleware() {
423
+ this.app.use(this.middleware);
424
+ }
425
+
426
+ setupAfterFeature() {
427
+ // Todo rename onAfterSetupMiddleware in next major release
428
+ // Todo pass only `this` argument
429
+ this.options.after(this.app, this, this.compiler);
430
+ }
431
+
432
+ setupHeadersFeature() {
433
+ this.app.all('*', this.setContentHeaders.bind(this));
434
+ }
304
435
 
436
+ setupMagicHtmlFeature() {
437
+ this.app.get('*', this.serveMagicHtml.bind(this));
438
+ }
439
+
440
+ setupSetupFeature() {
441
+ this.log.warn(
442
+ 'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`'
443
+ );
444
+
445
+ this.options.setup(this.app, this);
446
+ }
447
+
448
+ setupFeatures() {
305
449
  const features = {
306
450
  compress: () => {
307
- if (options.compress) {
308
- // Enable gzip compression.
309
- app.use(compress());
451
+ if (this.options.compress) {
452
+ this.setupCompressFeature();
310
453
  }
311
454
  },
312
455
  proxy: () => {
313
- if (options.proxy) {
314
- /**
315
- * Assume a proxy configuration specified as:
316
- * proxy: {
317
- * 'context': { options }
318
- * }
319
- * OR
320
- * proxy: {
321
- * 'context': 'target'
322
- * }
323
- */
324
- if (!Array.isArray(options.proxy)) {
325
- if (Object.prototype.hasOwnProperty.call(options.proxy, 'target')) {
326
- options.proxy = [options.proxy];
327
- } else {
328
- options.proxy = Object.keys(options.proxy).map((context) => {
329
- let proxyOptions;
330
- // For backwards compatibility reasons.
331
- const correctedContext = context
332
- .replace(/^\*$/, '**')
333
- .replace(/\/\*$/, '');
334
-
335
- if (typeof options.proxy[context] === 'string') {
336
- proxyOptions = {
337
- context: correctedContext,
338
- target: options.proxy[context],
339
- };
340
- } else {
341
- proxyOptions = Object.assign({}, options.proxy[context]);
342
- proxyOptions.context = correctedContext;
343
- }
344
-
345
- proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
346
-
347
- return proxyOptions;
348
- });
349
- }
350
- }
351
-
352
- const getProxyMiddleware = (proxyConfig) => {
353
- const context = proxyConfig.context || proxyConfig.path;
354
- // It is possible to use the `bypass` method without a `target`.
355
- // However, the proxy middleware has no use in this case, and will fail to instantiate.
356
- if (proxyConfig.target) {
357
- return httpProxyMiddleware(context, proxyConfig);
358
- }
359
- };
360
- /**
361
- * Assume a proxy configuration specified as:
362
- * proxy: [
363
- * {
364
- * context: ...,
365
- * ...options...
366
- * },
367
- * // or:
368
- * function() {
369
- * return {
370
- * context: ...,
371
- * ...options...
372
- * };
373
- * }
374
- * ]
375
- */
376
- options.proxy.forEach((proxyConfigOrCallback) => {
377
- let proxyConfig;
378
- let proxyMiddleware;
379
-
380
- if (typeof proxyConfigOrCallback === 'function') {
381
- proxyConfig = proxyConfigOrCallback();
382
- } else {
383
- proxyConfig = proxyConfigOrCallback;
384
- }
385
-
386
- proxyMiddleware = getProxyMiddleware(proxyConfig);
387
-
388
- if (proxyConfig.ws) {
389
- websocketProxies.push(proxyMiddleware);
390
- }
391
-
392
- app.use((req, res, next) => {
393
- if (typeof proxyConfigOrCallback === 'function') {
394
- const newProxyConfig = proxyConfigOrCallback();
395
-
396
- if (newProxyConfig !== proxyConfig) {
397
- proxyConfig = newProxyConfig;
398
- proxyMiddleware = getProxyMiddleware(proxyConfig);
399
- }
400
- }
401
-
402
- // - Check if we have a bypass function defined
403
- // - In case the bypass function is defined we'll retrieve the
404
- // bypassUrl from it otherwise byPassUrl would be null
405
- const isByPassFuncDefined =
406
- typeof proxyConfig.bypass === 'function';
407
- const bypassUrl = isByPassFuncDefined
408
- ? proxyConfig.bypass(req, res, proxyConfig)
409
- : null;
410
-
411
- if (typeof bypassUrl === 'boolean') {
412
- // skip the proxy
413
- req.url = null;
414
- next();
415
- } else if (typeof bypassUrl === 'string') {
416
- // byPass to that url
417
- req.url = bypassUrl;
418
- next();
419
- } else if (proxyMiddleware) {
420
- return proxyMiddleware(req, res, next);
421
- } else {
422
- next();
423
- }
424
- });
425
- });
456
+ if (this.options.proxy) {
457
+ this.setupProxyFeature();
426
458
  }
427
459
  },
428
460
  historyApiFallback: () => {
429
- if (options.historyApiFallback) {
430
- const fallback =
431
- typeof options.historyApiFallback === 'object'
432
- ? options.historyApiFallback
433
- : null;
434
- // Fall back to /index.html if nothing else matches.
435
- app.use(historyApiFallback(fallback));
461
+ if (this.options.historyApiFallback) {
462
+ this.setupHistoryApiFallbackFeature();
436
463
  }
437
464
  },
465
+ // Todo rename to `static` in future major release
438
466
  contentBaseFiles: () => {
439
- if (Array.isArray(contentBase)) {
440
- contentBase.forEach((item) => {
441
- app.get('*', express.static(item));
442
- });
443
- } else if (/^(https?:)?\/\//.test(contentBase)) {
444
- this.log.warn(
445
- 'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
446
- );
447
-
448
- this.log.warn(
449
- 'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
450
- );
451
- // Redirect every request to contentBase
452
- app.get('*', (req, res) => {
453
- res.writeHead(302, {
454
- Location: contentBase + req.path + (req._parsedUrl.search || ''),
455
- });
456
-
457
- res.end();
458
- });
459
- } else if (typeof contentBase === 'number') {
460
- this.log.warn(
461
- 'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
462
- );
463
-
464
- this.log.warn(
465
- 'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
466
- );
467
- // Redirect every request to the port contentBase
468
- app.get('*', (req, res) => {
469
- res.writeHead(302, {
470
- Location: `//localhost:${contentBase}${req.path}${req._parsedUrl
471
- .search || ''}`,
472
- });
473
-
474
- res.end();
475
- });
476
- } else {
477
- // route content request
478
- app.get('*', express.static(contentBase, options.staticOptions));
479
- }
467
+ this.setupStaticFeature();
480
468
  },
469
+ // Todo rename to `serveIndex` in future major release
481
470
  contentBaseIndex: () => {
482
- if (Array.isArray(contentBase)) {
483
- contentBase.forEach((item) => {
484
- app.get('*', serveIndex(item));
485
- });
486
- } else if (
487
- !/^(https?:)?\/\//.test(contentBase) &&
488
- typeof contentBase !== 'number'
489
- ) {
490
- app.get('*', serveIndex(contentBase));
491
- }
471
+ this.setupServeIndexFeature();
492
472
  },
473
+ // Todo rename to `watchStatic` in future major release
493
474
  watchContentBase: () => {
494
- if (
495
- /^(https?:)?\/\//.test(contentBase) ||
496
- typeof contentBase === 'number'
497
- ) {
498
- throw new Error('Watching remote files is not supported.');
499
- } else if (Array.isArray(contentBase)) {
500
- contentBase.forEach((item) => {
501
- this._watch(item);
502
- });
503
- } else {
504
- this._watch(contentBase);
505
- }
475
+ this.setupWatchStaticFeature();
506
476
  },
507
477
  before: () => {
508
- if (typeof options.before === 'function') {
509
- options.before(app, this, compiler);
478
+ if (typeof this.options.before === 'function') {
479
+ this.setupBeforeFeature();
510
480
  }
511
481
  },
512
482
  middleware: () => {
513
483
  // include our middleware to ensure
514
484
  // it is able to handle '/index.html' request after redirect
515
- app.use(this.middleware);
485
+ this.setupMiddleware();
516
486
  },
517
487
  after: () => {
518
- if (typeof options.after === 'function') {
519
- options.after(app, this, compiler);
488
+ if (typeof this.options.after === 'function') {
489
+ this.setupAfterFeature();
520
490
  }
521
491
  },
522
492
  headers: () => {
523
- app.all('*', this.setContentHeaders.bind(this));
493
+ this.setupHeadersFeature();
524
494
  },
525
495
  magicHtml: () => {
526
- app.get('*', this.serveMagicHtml.bind(this));
496
+ this.setupMagicHtmlFeature();
527
497
  },
528
498
  setup: () => {
529
- if (typeof options.setup === 'function') {
530
- this.log.warn(
531
- 'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`'
532
- );
533
-
534
- options.setup(app, this);
499
+ if (typeof this.options.setup === 'function') {
500
+ this.setupSetupFeature();
535
501
  }
536
502
  },
537
503
  };
538
504
 
539
- const defaultFeatures = ['setup', 'before', 'headers', 'middleware'];
505
+ const runnableFeatures = [];
540
506
 
541
- if (options.proxy) {
542
- defaultFeatures.push('proxy', 'middleware');
507
+ // compress is placed last and uses unshift so that it will be the first middleware used
508
+ if (this.options.compress) {
509
+ runnableFeatures.push('compress');
543
510
  }
544
511
 
545
- if (contentBase !== false) {
546
- defaultFeatures.push('contentBaseFiles');
512
+ runnableFeatures.push('setup', 'before', 'headers', 'middleware');
513
+
514
+ if (this.options.proxy) {
515
+ runnableFeatures.push('proxy', 'middleware');
547
516
  }
548
517
 
549
- if (options.watchContentBase) {
550
- defaultFeatures.push('watchContentBase');
518
+ if (this.options.contentBase !== false) {
519
+ runnableFeatures.push('contentBaseFiles');
551
520
  }
552
521
 
553
- if (options.historyApiFallback) {
554
- defaultFeatures.push('historyApiFallback', 'middleware');
522
+ if (this.options.historyApiFallback) {
523
+ runnableFeatures.push('historyApiFallback', 'middleware');
555
524
 
556
- if (contentBase !== false) {
557
- defaultFeatures.push('contentBaseFiles');
525
+ if (this.options.contentBase !== false) {
526
+ runnableFeatures.push('contentBaseFiles');
558
527
  }
559
528
  }
560
529
 
561
- defaultFeatures.push('magicHtml');
562
-
563
530
  // checking if it's set to true or not set (Default : undefined => true)
564
531
  this.serveIndex = this.serveIndex || this.serveIndex === undefined;
565
532
 
566
- const shouldHandleServeIndex = contentBase && this.serveIndex;
567
-
568
- if (shouldHandleServeIndex) {
569
- defaultFeatures.push('contentBaseIndex');
533
+ if (this.options.contentBase && this.serveIndex) {
534
+ runnableFeatures.push('contentBaseIndex');
570
535
  }
571
536
 
572
- // compress is placed last and uses unshift so that it will be the first middleware used
573
- if (options.compress) {
574
- defaultFeatures.unshift('compress');
537
+ if (this.options.watchContentBase) {
538
+ runnableFeatures.push('watchContentBase');
575
539
  }
576
540
 
577
- if (options.after) {
578
- defaultFeatures.push('after');
541
+ runnableFeatures.push('magicHtml');
542
+
543
+ if (this.options.after) {
544
+ runnableFeatures.push('after');
579
545
  }
580
546
 
581
- (options.features || defaultFeatures).forEach((feature) => {
547
+ (this.options.features || runnableFeatures).forEach((feature) => {
582
548
  features[feature]();
583
549
  });
550
+ }
551
+
552
+ setupHttps() {
553
+ // if the user enables http2, we can safely enable https
554
+ if (this.options.http2 && !this.options.https) {
555
+ this.options.https = true;
556
+ }
584
557
 
585
- if (options.https) {
558
+ if (this.options.https) {
586
559
  // for keep supporting CLI parameters
587
- if (typeof options.https === 'boolean') {
588
- options.https = {
589
- ca: options.ca,
590
- pfx: options.pfx,
591
- key: options.key,
592
- cert: options.cert,
593
- passphrase: options.pfxPassphrase,
594
- requestCert: options.requestCert || false,
560
+ if (typeof this.options.https === 'boolean') {
561
+ this.options.https = {
562
+ ca: this.options.ca,
563
+ pfx: this.options.pfx,
564
+ key: this.options.key,
565
+ cert: this.options.cert,
566
+ passphrase: this.options.pfxPassphrase,
567
+ requestCert: this.options.requestCert || false,
595
568
  };
596
569
  }
597
570
 
598
571
  for (const property of ['ca', 'pfx', 'key', 'cert']) {
599
- const value = options.https[property];
572
+ const value = this.options.https[property];
600
573
  const isBuffer = value instanceof Buffer;
601
574
 
602
575
  if (value && !isBuffer) {
603
576
  let stats = null;
604
577
 
605
578
  try {
606
- stats = fs.lstatSync(value).isFile();
579
+ stats = fs.lstatSync(fs.realpathSync(value)).isFile();
607
580
  } catch (error) {
608
581
  // ignore error
609
582
  }
610
583
 
611
- if (stats) {
612
- // It is file
613
- options.https[property] = fs.readFileSync(path.resolve(value));
614
- } else {
615
- options.https[property] = value;
616
- }
584
+ // It is file
585
+ this.options.https[property] = stats
586
+ ? fs.readFileSync(path.resolve(value))
587
+ : value;
617
588
  }
618
589
  }
619
590
 
620
591
  let fakeCert;
621
592
 
622
- if (!options.https.key || !options.https.cert) {
623
- // Use a self-signed certificate if no certificate was configured.
624
- // Cycle certs every 24 hours
625
- const certPath = path.join(__dirname, '../ssl/server.pem');
626
-
627
- let certExists = fs.existsSync(certPath);
628
-
629
- if (certExists) {
630
- const certTtl = 1000 * 60 * 60 * 24;
631
- const certStat = fs.statSync(certPath);
632
-
633
- const now = new Date();
634
-
635
- // cert is more than 30 days old, kill it with fire
636
- if ((now - certStat.ctime) / certTtl > 30) {
637
- this.log.info(
638
- 'SSL Certificate is more than 30 days old. Removing.'
639
- );
640
-
641
- del.sync([certPath], { force: true });
642
-
643
- certExists = false;
644
- }
645
- }
646
-
647
- if (!certExists) {
648
- this.log.info('Generating SSL Certificate');
649
-
650
- const attrs = [{ name: 'commonName', value: 'localhost' }];
651
-
652
- const pems = createCertificate(attrs);
653
-
654
- fs.writeFileSync(certPath, pems.private + pems.cert, {
655
- encoding: 'utf8',
656
- });
657
- }
658
-
659
- fakeCert = fs.readFileSync(certPath);
593
+ if (!this.options.https.key || !this.options.https.cert) {
594
+ fakeCert = getCertificate(this.log);
660
595
  }
661
596
 
662
- options.https.key = options.https.key || fakeCert;
663
- options.https.cert = options.https.cert || fakeCert;
664
-
665
- // Only prevent HTTP/2 if http2 is explicitly set to false
666
- const isHttp2 = options.http2 !== false;
597
+ this.options.https.key = this.options.https.key || fakeCert;
598
+ this.options.https.cert = this.options.https.cert || fakeCert;
667
599
 
668
600
  // note that options.spdy never existed. The user was able
669
601
  // to set options.https.spdy before, though it was not in the
670
602
  // docs. Keep options.https.spdy if the user sets it for
671
- // backwards compatability, but log a deprecation warning.
672
- if (options.https.spdy) {
673
- // for backwards compatability: if options.https.spdy was passed in before,
603
+ // backwards compatibility, but log a deprecation warning.
604
+ if (this.options.https.spdy) {
605
+ // for backwards compatibility: if options.https.spdy was passed in before,
674
606
  // it was not altered in any way
675
607
  this.log.warn(
676
608
  'Providing custom spdy server options is deprecated and will be removed in the next major version.'
677
609
  );
678
610
  } else {
679
611
  // if the normal https server gets this option, it will not affect it.
680
- options.https.spdy = {
612
+ this.options.https.spdy = {
681
613
  protocols: ['h2', 'http/1.1'],
682
614
  };
683
615
  }
616
+ }
617
+ }
618
+
619
+ createServer() {
620
+ if (this.options.https) {
621
+ // Only prevent HTTP/2 if http2 is explicitly set to false
622
+ const isHttp2 = this.options.http2 !== false;
684
623
 
685
624
  // `spdy` is effectively unmaintained, and as a consequence of an
686
625
  // implementation that extensively relies on Node’s non-public APIs, broken
@@ -692,33 +631,150 @@ class Server {
692
631
  // - https://github.com/webpack/webpack-dev-server/issues/1449
693
632
  // - https://github.com/expressjs/express/issues/3388
694
633
  if (semver.gte(process.version, '10.0.0') || !isHttp2) {
695
- if (options.http2) {
634
+ if (this.options.http2) {
696
635
  // the user explicitly requested http2 but is not getting it because
697
636
  // of the node version.
698
637
  this.log.warn(
699
638
  'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it'
700
639
  );
701
640
  }
702
- this.listeningApp = https.createServer(options.https, app);
641
+ this.listeningApp = https.createServer(this.options.https, this.app);
703
642
  } else {
704
- /* eslint-disable global-require */
705
643
  // The relevant issues are:
706
644
  // https://github.com/spdy-http2/node-spdy/issues/350
707
645
  // https://github.com/webpack/webpack-dev-server/issues/1592
708
- this.listeningApp = require('spdy').createServer(options.https, app);
709
- /* eslint-enable global-require */
646
+ // eslint-disable-next-line global-require
647
+ this.listeningApp = require('spdy').createServer(
648
+ this.options.https,
649
+ this.app
650
+ );
710
651
  }
711
652
  } else {
712
- this.listeningApp = http.createServer(app);
653
+ this.listeningApp = http.createServer(this.app);
713
654
  }
655
+ }
714
656
 
715
- killable(this.listeningApp);
657
+ createSocketServer() {
658
+ this.socketServer = new SockJSServer(this);
716
659
 
717
- // Proxy websockets without the initial http request
718
- // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
719
- websocketProxies.forEach(function(wsProxy) {
720
- this.listeningApp.on('upgrade', wsProxy.upgrade);
721
- }, this);
660
+ this.socketServer.onConnection((connection) => {
661
+ if (!connection) {
662
+ return;
663
+ }
664
+
665
+ if (
666
+ !this.checkHost(connection.headers) ||
667
+ !this.checkOrigin(connection.headers)
668
+ ) {
669
+ this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
670
+
671
+ connection.close();
672
+
673
+ return;
674
+ }
675
+
676
+ this.sockets.push(connection);
677
+
678
+ connection.on('close', () => {
679
+ const idx = this.sockets.indexOf(connection);
680
+
681
+ if (idx >= 0) {
682
+ this.sockets.splice(idx, 1);
683
+ }
684
+ });
685
+
686
+ if (this.hot) {
687
+ this.sockWrite([connection], 'hot');
688
+ }
689
+
690
+ if (this.options.liveReload !== false) {
691
+ this.sockWrite([connection], 'liveReload', this.options.liveReload);
692
+ }
693
+
694
+ if (this.progress) {
695
+ this.sockWrite([connection], 'progress', this.progress);
696
+ }
697
+
698
+ if (this.clientOverlay) {
699
+ this.sockWrite([connection], 'overlay', this.clientOverlay);
700
+ }
701
+
702
+ if (this.clientLogLevel) {
703
+ this.sockWrite([connection], 'log-level', this.clientLogLevel);
704
+ }
705
+
706
+ if (!this._stats) {
707
+ return;
708
+ }
709
+
710
+ this._sendStats([connection], this.getStats(this._stats), true);
711
+ });
712
+ }
713
+
714
+ showStatus() {
715
+ const suffix =
716
+ this.options.inline !== false || this.options.lazy === true
717
+ ? '/'
718
+ : '/webpack-dev-server/';
719
+ const uri = `${createDomain(this.options, this.listeningApp)}${suffix}`;
720
+
721
+ status(
722
+ uri,
723
+ this.options,
724
+ this.log,
725
+ this.options.stats && this.options.stats.colors
726
+ );
727
+ }
728
+
729
+ listen(port, hostname, fn) {
730
+ this.hostname = hostname;
731
+
732
+ return this.listeningApp.listen(port, hostname, (err) => {
733
+ this.createSocketServer();
734
+
735
+ if (this.options.bonjour) {
736
+ runBonjour(this.options);
737
+ }
738
+
739
+ this.showStatus();
740
+
741
+ if (fn) {
742
+ fn.call(this.listeningApp, err);
743
+ }
744
+
745
+ if (typeof this.options.onListening === 'function') {
746
+ this.options.onListening(this);
747
+ }
748
+ });
749
+ }
750
+
751
+ close(cb) {
752
+ this.sockets.forEach((socket) => {
753
+ this.socketServer.close(socket);
754
+ });
755
+
756
+ this.sockets = [];
757
+
758
+ this.contentBaseWatchers.forEach((watcher) => {
759
+ watcher.close();
760
+ });
761
+
762
+ this.contentBaseWatchers = [];
763
+
764
+ this.listeningApp.kill(() => {
765
+ this.middleware.close(cb);
766
+ });
767
+ }
768
+
769
+ static get DEFAULT_STATS() {
770
+ return {
771
+ all: false,
772
+ hash: true,
773
+ assets: true,
774
+ warnings: true,
775
+ errors: true,
776
+ errorDetails: false,
777
+ };
722
778
  }
723
779
 
724
780
  getStats(statsObj) {
@@ -740,7 +796,6 @@ class Server {
740
796
  if (this.headers) {
741
797
  // eslint-disable-next-line
742
798
  for (const name in this.headers) {
743
- // eslint-disable-line
744
799
  res.setHeader(name, this.headers[name]);
745
800
  }
746
801
  }
@@ -790,7 +845,7 @@ class Server {
790
845
  if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) {
791
846
  return true;
792
847
  }
793
- // always allow localhost host, for convience
848
+ // always allow localhost host, for convenience
794
849
  if (hostname === 'localhost') {
795
850
  return true;
796
851
  }
@@ -818,7 +873,7 @@ class Server {
818
873
  }
819
874
  }
820
875
 
821
- // allow hostname of listening adress
876
+ // allow hostname of listening address
822
877
  if (hostname === this.hostname) {
823
878
  return true;
824
879
  }
@@ -826,7 +881,6 @@ class Server {
826
881
  // also allow public hostname if provided
827
882
  if (typeof this.publicHost === 'string') {
828
883
  const idxPublic = this.publicHost.indexOf(':');
829
-
830
884
  const publicHostname =
831
885
  idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
832
886
 
@@ -839,105 +893,10 @@ class Server {
839
893
  return false;
840
894
  }
841
895
 
842
- // delegate listen call and init sockjs
843
- listen(port, hostname, fn) {
844
- this.hostname = hostname;
845
-
846
- return this.listeningApp.listen(port, hostname, (err) => {
847
- const socket = sockjs.createServer({
848
- // Use provided up-to-date sockjs-client
849
- sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
850
- // Limit useless logs
851
- log: (severity, line) => {
852
- if (severity === 'error') {
853
- this.log.error(line);
854
- } else {
855
- this.log.debug(line);
856
- }
857
- },
858
- });
859
-
860
- socket.on('connection', (connection) => {
861
- if (!connection) {
862
- return;
863
- }
864
-
865
- if (
866
- !this.checkHost(connection.headers) ||
867
- !this.checkOrigin(connection.headers)
868
- ) {
869
- this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
870
-
871
- connection.close();
872
-
873
- return;
874
- }
875
-
876
- this.sockets.push(connection);
877
-
878
- connection.on('close', () => {
879
- const idx = this.sockets.indexOf(connection);
880
-
881
- if (idx >= 0) {
882
- this.sockets.splice(idx, 1);
883
- }
884
- });
885
-
886
- if (this.hot) {
887
- this.sockWrite([connection], 'hot');
888
- }
889
-
890
- if (this.progress) {
891
- this.sockWrite([connection], 'progress', this.progress);
892
- }
893
-
894
- if (this.clientOverlay) {
895
- this.sockWrite([connection], 'overlay', this.clientOverlay);
896
- }
897
-
898
- if (this.clientLogLevel) {
899
- this.sockWrite([connection], 'log-level', this.clientLogLevel);
900
- }
901
-
902
- if (!this._stats) {
903
- return;
904
- }
905
-
906
- this._sendStats([connection], this.getStats(this._stats), true);
907
- });
908
-
909
- socket.installHandlers(this.listeningApp, {
910
- prefix: this.sockPath,
911
- });
912
-
913
- if (fn) {
914
- fn.call(this.listeningApp, err);
915
- }
916
- });
917
- }
918
-
919
- close(cb) {
920
- this.sockets.forEach((socket) => {
921
- socket.close();
922
- });
923
-
924
- this.sockets = [];
925
-
926
- this.contentBaseWatchers.forEach((watcher) => {
927
- watcher.close();
928
- });
929
-
930
- this.contentBaseWatchers = [];
931
-
932
- this.listeningApp.kill(() => {
933
- this.middleware.close(cb);
934
- });
935
- }
936
-
937
896
  // eslint-disable-next-line
938
897
  sockWrite(sockets, type, data) {
939
898
  sockets.forEach((socket) => {
940
- socket.write(JSON.stringify({ type, data }));
899
+ this.socketServer.send(socket, JSON.stringify({ type, data }));
941
900
  });
942
901
  }
943
902
 
@@ -999,7 +958,7 @@ class Server {
999
958
  ? this.watchOptions.poll
1000
959
  : undefined;
1001
960
 
1002
- const options = {
961
+ const watchOptions = {
1003
962
  ignoreInitial: true,
1004
963
  persistent: true,
1005
964
  followSymlinks: false,
@@ -1011,18 +970,19 @@ class Server {
1011
970
  interval,
1012
971
  };
1013
972
 
1014
- const watcher = chokidar.watch(watchPath, options);
1015
-
1016
- watcher.on('change', () => {
1017
- this.sockWrite(this.sockets, 'content-changed');
1018
- });
1019
-
973
+ const watcher = chokidar.watch(watchPath, watchOptions);
974
+ // disabling refreshing on changing the content
975
+ if (this.options.liveReload !== false) {
976
+ watcher.on('change', () => {
977
+ this.sockWrite(this.sockets, 'content-changed');
978
+ });
979
+ }
1020
980
  this.contentBaseWatchers.push(watcher);
1021
981
  }
1022
982
 
1023
- invalidate() {
983
+ invalidate(callback) {
1024
984
  if (this.middleware) {
1025
- this.middleware.invalidate();
985
+ this.middleware.invalidate(callback);
1026
986
  }
1027
987
  }
1028
988
  }