Server.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. 'use strict';
  2. /* eslint func-names: off */
  3. require('./polyfills');
  4. const fs = require('fs');
  5. const http = require('http');
  6. const path = require('path');
  7. const url = require('url');
  8. const chokidar = require('chokidar');
  9. const compress = require('compression');
  10. const del = require('del');
  11. const express = require('express');
  12. const httpProxyMiddleware = require('http-proxy-middleware');
  13. const ip = require('ip');
  14. const killable = require('killable');
  15. const serveIndex = require('serve-index');
  16. const historyApiFallback = require('connect-history-api-fallback');
  17. const selfsigned = require('selfsigned');
  18. const sockjs = require('sockjs');
  19. const spdy = require('spdy');
  20. const webpack = require('webpack');
  21. const webpackDevMiddleware = require('webpack-dev-middleware');
  22. const OptionsValidationError = require('./OptionsValidationError');
  23. const optionsSchema = require('./optionsSchema.json');
  24. const clientStats = { errorDetails: false };
  25. const log = console.log; // eslint-disable-line no-console
  26. function Server(compiler, options) {
  27. // Default options
  28. if (!options) options = {};
  29. const validationErrors = webpack.validateSchema(optionsSchema, options);
  30. if (validationErrors.length) {
  31. throw new OptionsValidationError(validationErrors);
  32. }
  33. if (options.lazy && !options.filename) {
  34. throw new Error("'filename' option must be set in lazy mode.");
  35. }
  36. this.hot = options.hot || options.hotOnly;
  37. this.headers = options.headers;
  38. this.clientLogLevel = options.clientLogLevel;
  39. this.clientOverlay = options.overlay;
  40. this.progress = options.progress;
  41. this.disableHostCheck = !!options.disableHostCheck;
  42. this.publicHost = options.public;
  43. this.allowedHosts = options.allowedHosts;
  44. this.sockets = [];
  45. this.contentBaseWatchers = [];
  46. this.watchOptions = options.watchOptions || {};
  47. // Listening for events
  48. const invalidPlugin = () => {
  49. this.sockWrite(this.sockets, 'invalid');
  50. };
  51. if (this.progress) {
  52. const progressPlugin = new webpack.ProgressPlugin((percent, msg, addInfo) => {
  53. percent = Math.floor(percent * 100);
  54. if (percent === 100) msg = 'Compilation completed';
  55. if (addInfo) msg = `${msg} (${addInfo})`;
  56. this.sockWrite(this.sockets, 'progress-update', { percent, msg });
  57. });
  58. compiler.apply(progressPlugin);
  59. }
  60. compiler.plugin('compile', invalidPlugin);
  61. compiler.plugin('invalid', invalidPlugin);
  62. compiler.plugin('done', (stats) => {
  63. this._sendStats(this.sockets, stats.toJson(clientStats));
  64. this._stats = stats;
  65. });
  66. // Init express server
  67. const app = this.app = new express(); // eslint-disable-line
  68. app.all('*', (req, res, next) => { // eslint-disable-line
  69. if (this.checkHost(req.headers)) { return next(); }
  70. res.send('Invalid Host header');
  71. });
  72. const wdmOptions = {};
  73. if (options.quiet === true) {
  74. wdmOptions.logLevel = 'silent';
  75. }
  76. if (options.noInfo === true) {
  77. wdmOptions.logLevel = 'warn';
  78. }
  79. // middleware for serving webpack bundle
  80. this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));
  81. app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
  82. res.setHeader('Content-Type', 'application/javascript');
  83. fs.createReadStream(path.join(__dirname, '..', 'client', 'live.bundle.js')).pipe(res);
  84. });
  85. app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
  86. res.setHeader('Content-Type', 'application/javascript');
  87. fs.createReadStream(path.join(__dirname, '..', 'client', 'sockjs.bundle.js')).pipe(res);
  88. });
  89. app.get('/webpack-dev-server.js', (req, res) => {
  90. res.setHeader('Content-Type', 'application/javascript');
  91. fs.createReadStream(path.join(__dirname, '..', 'client', 'index.bundle.js')).pipe(res);
  92. });
  93. app.get('/webpack-dev-server/*', (req, res) => {
  94. res.setHeader('Content-Type', 'text/html');
  95. fs.createReadStream(path.join(__dirname, '..', 'client', 'live.html')).pipe(res);
  96. });
  97. app.get('/webpack-dev-server', (req, res) => {
  98. res.setHeader('Content-Type', 'text/html');
  99. res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
  100. const outputPath = this.middleware.getFilenameFromUrl(options.publicPath || '/');
  101. const filesystem = this.middleware.fileSystem;
  102. function writeDirectory(baseUrl, basePath) {
  103. const content = filesystem.readdirSync(basePath);
  104. res.write('<ul>');
  105. content.forEach((item) => {
  106. const p = `${basePath}/${item}`;
  107. if (filesystem.statSync(p).isFile()) {
  108. res.write('<li><a href="');
  109. res.write(baseUrl + item);
  110. res.write('">');
  111. res.write(item);
  112. res.write('</a></li>');
  113. if (/\.js$/.test(item)) {
  114. const htmlItem = item.substr(0, item.length - 3);
  115. res.write('<li><a href="');
  116. res.write(baseUrl + htmlItem);
  117. res.write('">');
  118. res.write(htmlItem);
  119. res.write('</a> (magic html for ');
  120. res.write(item);
  121. res.write(') (<a href="');
  122. res.write(baseUrl.replace(/(^(https?:\/\/[^\/]+)?\/)/, "$1webpack-dev-server/") + htmlItem); // eslint-disable-line
  123. res.write('">webpack-dev-server</a>)</li>');
  124. }
  125. } else {
  126. res.write('<li>');
  127. res.write(item);
  128. res.write('<br>');
  129. writeDirectory(`${baseUrl + item}/`, p);
  130. res.write('</li>');
  131. }
  132. });
  133. res.write('</ul>');
  134. }
  135. writeDirectory(options.publicPath || '/', outputPath);
  136. res.end('</body></html>');
  137. });
  138. let contentBase;
  139. if (options.contentBase !== undefined) { // eslint-disable-line
  140. contentBase = options.contentBase; // eslint-disable-line
  141. } else {
  142. contentBase = process.cwd();
  143. }
  144. // Keep track of websocket proxies for external websocket upgrade.
  145. const websocketProxies = [];
  146. const features = {
  147. compress() {
  148. if (options.compress) {
  149. // Enable gzip compression.
  150. app.use(compress());
  151. }
  152. },
  153. proxy() {
  154. if (options.proxy) {
  155. /**
  156. * Assume a proxy configuration specified as:
  157. * proxy: {
  158. * 'context': { options }
  159. * }
  160. * OR
  161. * proxy: {
  162. * 'context': 'target'
  163. * }
  164. */
  165. if (!Array.isArray(options.proxy)) {
  166. options.proxy = Object.keys(options.proxy).map((context) => {
  167. let proxyOptions;
  168. // For backwards compatibility reasons.
  169. const correctedContext = context.replace(/^\*$/, '**').replace(/\/\*$/, '');
  170. if (typeof options.proxy[context] === 'string') {
  171. proxyOptions = {
  172. context: correctedContext,
  173. target: options.proxy[context]
  174. };
  175. } else {
  176. proxyOptions = Object.assign({}, options.proxy[context]);
  177. proxyOptions.context = correctedContext;
  178. }
  179. proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
  180. return proxyOptions;
  181. });
  182. }
  183. const getProxyMiddleware = (proxyConfig) => {
  184. const context = proxyConfig.context || proxyConfig.path;
  185. // It is possible to use the `bypass` method without a `target`.
  186. // However, the proxy middleware has no use in this case, and will fail to instantiate.
  187. if (proxyConfig.target) {
  188. return httpProxyMiddleware(context, proxyConfig);
  189. }
  190. };
  191. /**
  192. * Assume a proxy configuration specified as:
  193. * proxy: [
  194. * {
  195. * context: ...,
  196. * ...options...
  197. * },
  198. * // or:
  199. * function() {
  200. * return {
  201. * context: ...,
  202. * ...options...
  203. * };
  204. * }
  205. * ]
  206. */
  207. options.proxy.forEach((proxyConfigOrCallback) => {
  208. let proxyConfig;
  209. let proxyMiddleware;
  210. if (typeof proxyConfigOrCallback === 'function') {
  211. proxyConfig = proxyConfigOrCallback();
  212. } else {
  213. proxyConfig = proxyConfigOrCallback;
  214. }
  215. proxyMiddleware = getProxyMiddleware(proxyConfig);
  216. if (proxyConfig.ws) {
  217. websocketProxies.push(proxyMiddleware);
  218. }
  219. app.use((req, res, next) => {
  220. if (typeof proxyConfigOrCallback === 'function') {
  221. const newProxyConfig = proxyConfigOrCallback();
  222. if (newProxyConfig !== proxyConfig) {
  223. proxyConfig = newProxyConfig;
  224. proxyMiddleware = getProxyMiddleware(proxyConfig);
  225. }
  226. }
  227. const bypass = typeof proxyConfig.bypass === 'function';
  228. // eslint-disable-next-line
  229. const bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
  230. if (bypassUrl) {
  231. req.url = bypassUrl;
  232. next();
  233. } else if (proxyMiddleware) {
  234. return proxyMiddleware(req, res, next);
  235. } else {
  236. next();
  237. }
  238. });
  239. });
  240. }
  241. },
  242. historyApiFallback() {
  243. if (options.historyApiFallback) {
  244. // Fall back to /index.html if nothing else matches.
  245. app.use(historyApiFallback(typeof options.historyApiFallback === 'object' ? options.historyApiFallback : null));
  246. }
  247. },
  248. contentBaseFiles() {
  249. if (Array.isArray(contentBase)) {
  250. contentBase.forEach((item) => {
  251. app.get('*', express.static(item));
  252. });
  253. } else if (/^(https?:)?\/\//.test(contentBase)) {
  254. log('Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
  255. log('proxy: {\n\t"*": "<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
  256. // Redirect every request to contentBase
  257. app.get('*', (req, res) => {
  258. res.writeHead(302, {
  259. Location: contentBase + req.path + (req._parsedUrl.search || '')
  260. });
  261. res.end();
  262. });
  263. } else if (typeof contentBase === 'number') {
  264. log('Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
  265. log('proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
  266. // Redirect every request to the port contentBase
  267. app.get('*', (req, res) => {
  268. res.writeHead(302, {
  269. Location: `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ''}`
  270. });
  271. res.end();
  272. });
  273. } else {
  274. // route content request
  275. app.get('*', express.static(contentBase, options.staticOptions));
  276. }
  277. },
  278. contentBaseIndex() {
  279. if (Array.isArray(contentBase)) {
  280. contentBase.forEach((item) => {
  281. app.get('*', serveIndex(item));
  282. });
  283. } else if (!/^(https?:)?\/\//.test(contentBase) && typeof contentBase !== 'number') {
  284. app.get('*', serveIndex(contentBase));
  285. }
  286. },
  287. watchContentBase: () => {
  288. if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {
  289. throw new Error('Watching remote files is not supported.');
  290. } else if (Array.isArray(contentBase)) {
  291. contentBase.forEach((item) => {
  292. this._watch(item);
  293. });
  294. } else {
  295. this._watch(contentBase);
  296. }
  297. },
  298. before: () => {
  299. if (typeof options.before === 'function') {
  300. options.before(app, this);
  301. }
  302. },
  303. middleware: () => {
  304. // include our middleware to ensure it is able to handle '/index.html' request after redirect
  305. app.use(this.middleware);
  306. },
  307. after: () => {
  308. if (typeof options.after === 'function') { options.after(app, this); }
  309. },
  310. headers: () => {
  311. app.all('*', this.setContentHeaders.bind(this));
  312. },
  313. magicHtml: () => {
  314. app.get('*', this.serveMagicHtml.bind(this));
  315. },
  316. setup: () => {
  317. if (typeof options.setup === 'function') {
  318. log('The `setup` option is deprecated and will be removed in v3. Please update your config to use `before`');
  319. options.setup(app, this);
  320. }
  321. }
  322. };
  323. const defaultFeatures = ['before', 'setup', 'headers', 'middleware'];
  324. if (options.proxy) { defaultFeatures.push('proxy', 'middleware'); }
  325. if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
  326. if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }
  327. if (options.historyApiFallback) {
  328. defaultFeatures.push('historyApiFallback', 'middleware');
  329. if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
  330. }
  331. defaultFeatures.push('magicHtml');
  332. if (contentBase !== false) { defaultFeatures.push('contentBaseIndex'); }
  333. // compress is placed last and uses unshift so that it will be the first middleware used
  334. if (options.compress) { defaultFeatures.unshift('compress'); }
  335. if (options.after) { defaultFeatures.push('after'); }
  336. (options.features || defaultFeatures).forEach((feature) => {
  337. features[feature]();
  338. });
  339. if (options.https) {
  340. // for keep supporting CLI parameters
  341. if (typeof options.https === 'boolean') {
  342. options.https = {
  343. key: options.key,
  344. cert: options.cert,
  345. ca: options.ca,
  346. pfx: options.pfx,
  347. passphrase: options.pfxPassphrase,
  348. requestCert: options.requestCert || false
  349. };
  350. }
  351. let fakeCert;
  352. if (!options.https.key || !options.https.cert) {
  353. // Use a self-signed certificate if no certificate was configured.
  354. // Cycle certs every 24 hours
  355. const certPath = path.join(__dirname, '../ssl/server.pem');
  356. let certExists = fs.existsSync(certPath);
  357. if (certExists) {
  358. const certStat = fs.statSync(certPath);
  359. const certTtl = 1000 * 60 * 60 * 24;
  360. const now = new Date();
  361. // cert is more than 30 days old, kill it with fire
  362. if ((now - certStat.ctime) / certTtl > 30) {
  363. log('SSL Certificate is more than 30 days old. Removing.');
  364. del.sync([certPath], { force: true });
  365. certExists = false;
  366. }
  367. }
  368. if (!certExists) {
  369. log('Generating SSL Certificate');
  370. const attrs = [{ name: 'commonName', value: 'localhost' }];
  371. const pems = selfsigned.generate(attrs, {
  372. algorithm: 'sha256',
  373. days: 30,
  374. keySize: 2048,
  375. extensions: [{
  376. name: 'basicConstraints',
  377. cA: true
  378. }, {
  379. name: 'keyUsage',
  380. keyCertSign: true,
  381. digitalSignature: true,
  382. nonRepudiation: true,
  383. keyEncipherment: true,
  384. dataEncipherment: true
  385. }, {
  386. name: 'subjectAltName',
  387. altNames: [
  388. {
  389. // type 2 is DNS
  390. type: 2,
  391. value: 'localhost'
  392. },
  393. {
  394. type: 2,
  395. value: 'localhost.localdomain'
  396. },
  397. {
  398. type: 2,
  399. value: 'lvh.me'
  400. },
  401. {
  402. type: 2,
  403. value: '*.lvh.me'
  404. },
  405. {
  406. type: 2,
  407. value: '[::1]'
  408. },
  409. {
  410. // type 7 is IP
  411. type: 7,
  412. ip: '127.0.0.1'
  413. },
  414. {
  415. type: 7,
  416. ip: 'fe80::1'
  417. }
  418. ]
  419. }]
  420. });
  421. fs.writeFileSync(certPath, pems.private + pems.cert, { encoding: 'utf-8' });
  422. }
  423. fakeCert = fs.readFileSync(certPath);
  424. }
  425. options.https.key = options.https.key || fakeCert;
  426. options.https.cert = options.https.cert || fakeCert;
  427. if (!options.https.spdy) {
  428. options.https.spdy = {
  429. protocols: ['h2', 'http/1.1']
  430. };
  431. }
  432. this.listeningApp = spdy.createServer(options.https, app);
  433. } else {
  434. this.listeningApp = http.createServer(app);
  435. }
  436. killable(this.listeningApp);
  437. // Proxy websockets without the initial http request
  438. // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
  439. websocketProxies.forEach(function (wsProxy) {
  440. this.listeningApp.on('upgrade', wsProxy.upgrade);
  441. }, this);
  442. }
  443. Server.prototype.use = function () {
  444. // eslint-disable-next-line
  445. this.app.use.apply(this.app, arguments);
  446. };
  447. Server.prototype.setContentHeaders = function (req, res, next) {
  448. if (this.headers) {
  449. for (const name in this.headers) { // eslint-disable-line
  450. res.setHeader(name, this.headers[name]);
  451. }
  452. }
  453. next();
  454. };
  455. Server.prototype.checkHost = function (headers) {
  456. // allow user to opt-out this security check, at own risk
  457. if (this.disableHostCheck) return true;
  458. // get the Host header and extract hostname
  459. // we don't care about port not matching
  460. const hostHeader = headers.host;
  461. if (!hostHeader) return false;
  462. // use the node url-parser to retrieve the hostname from the host-header.
  463. const hostname = url.parse(`//${hostHeader}`, false, true).hostname;
  464. // always allow requests with explicit IPv4 or IPv6-address.
  465. // A note on IPv6 addresses: hostHeader will always contain the brackets denoting
  466. // an IPv6-address in URLs, these are removed from the hostname in url.parse(),
  467. // so we have the pure IPv6-address in hostname.
  468. if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) return true;
  469. // always allow localhost host, for convience
  470. if (hostname === 'localhost') return true;
  471. // allow if hostname is in allowedHosts
  472. if (this.allowedHosts && this.allowedHosts.length) {
  473. for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
  474. const allowedHost = this.allowedHosts[hostIdx];
  475. if (allowedHost === hostname) return true;
  476. // support "." as a subdomain wildcard
  477. // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
  478. if (allowedHost[0] === '.') {
  479. // "example.com"
  480. if (hostname === allowedHost.substring(1)) return true;
  481. // "*.example.com"
  482. if (hostname.endsWith(allowedHost)) return true;
  483. }
  484. }
  485. }
  486. // allow hostname of listening adress
  487. if (hostname === this.listenHostname) return true;
  488. // also allow public hostname if provided
  489. if (typeof this.publicHost === 'string') {
  490. const idxPublic = this.publicHost.indexOf(':');
  491. const publicHostname = idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
  492. if (hostname === publicHostname) return true;
  493. }
  494. // disallow
  495. return false;
  496. };
  497. // delegate listen call and init sockjs
  498. Server.prototype.listen = function (port, hostname, fn) {
  499. this.listenHostname = hostname;
  500. // eslint-disable-next-line
  501. const returnValue = this.listeningApp.listen(port, hostname, (err) => {
  502. const sockServer = sockjs.createServer({
  503. // Use provided up-to-date sockjs-client
  504. sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
  505. // Limit useless logs
  506. log(severity, line) {
  507. if (severity === 'error') {
  508. log(line);
  509. }
  510. }
  511. });
  512. sockServer.on('connection', (conn) => {
  513. if (!conn) return;
  514. if (!this.checkHost(conn.headers)) {
  515. this.sockWrite([conn], 'error', 'Invalid Host header');
  516. conn.close();
  517. return;
  518. }
  519. this.sockets.push(conn);
  520. conn.on('close', () => {
  521. const connIndex = this.sockets.indexOf(conn);
  522. if (connIndex >= 0) {
  523. this.sockets.splice(connIndex, 1);
  524. }
  525. });
  526. if (this.clientLogLevel) { this.sockWrite([conn], 'log-level', this.clientLogLevel); }
  527. if (this.progress) { this.sockWrite([conn], 'progress', this.progress); }
  528. if (this.clientOverlay) { this.sockWrite([conn], 'overlay', this.clientOverlay); }
  529. if (this.hot) this.sockWrite([conn], 'hot');
  530. if (!this._stats) return;
  531. this._sendStats([conn], this._stats.toJson(clientStats), true);
  532. });
  533. sockServer.installHandlers(this.listeningApp, {
  534. prefix: '/sockjs-node'
  535. });
  536. if (fn) {
  537. fn.call(this.listeningApp, err);
  538. }
  539. });
  540. return returnValue;
  541. };
  542. Server.prototype.close = function (callback) {
  543. this.sockets.forEach((sock) => {
  544. sock.close();
  545. });
  546. this.sockets = [];
  547. this.contentBaseWatchers.forEach((watcher) => {
  548. watcher.close();
  549. });
  550. this.contentBaseWatchers = [];
  551. this.listeningApp.kill(() => {
  552. this.middleware.close(callback);
  553. });
  554. };
  555. Server.prototype.sockWrite = function (sockets, type, data) {
  556. sockets.forEach((sock) => {
  557. sock.write(JSON.stringify({
  558. type,
  559. data
  560. }));
  561. });
  562. };
  563. Server.prototype.serveMagicHtml = function (req, res, next) {
  564. const _path = req.path;
  565. try {
  566. if (!this.middleware.fileSystem.statSync(this.middleware.getFilenameFromUrl(`${_path}.js`)).isFile()) { return next(); }
  567. // Serve a page that executes the javascript
  568. res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="');
  569. res.write(_path);
  570. res.write('.js');
  571. res.write(req._parsedUrl.search || '');
  572. res.end('"></script></body></html>');
  573. } catch (e) {
  574. return next();
  575. }
  576. };
  577. // send stats to a socket or multiple sockets
  578. Server.prototype._sendStats = function (sockets, stats, force) {
  579. if (!force &&
  580. stats &&
  581. (!stats.errors || stats.errors.length === 0) &&
  582. stats.assets &&
  583. stats.assets.every(asset => !asset.emitted)
  584. ) { return this.sockWrite(sockets, 'still-ok'); }
  585. this.sockWrite(sockets, 'hash', stats.hash);
  586. if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
  587. };
  588. Server.prototype._watch = function (watchPath) {
  589. // duplicate the same massaging of options that watchpack performs
  590. // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
  591. // this isn't an elegant solution, but we'll improve it in the future
  592. const usePolling = this.watchOptions.poll ? true : undefined; // eslint-disable-line no-undefined
  593. const interval = typeof this.watchOptions.poll === 'number' ? this.watchOptions.poll : undefined; // eslint-disable-line no-undefined
  594. const options = {
  595. ignoreInitial: true,
  596. persistent: true,
  597. followSymlinks: false,
  598. depth: 0,
  599. atomic: false,
  600. alwaysStat: true,
  601. ignorePermissionErrors: true,
  602. ignored: this.watchOptions.ignored,
  603. usePolling,
  604. interval
  605. };
  606. const watcher = chokidar.watch(watchPath, options).on('change', () => {
  607. this.sockWrite(this.sockets, 'content-changed');
  608. });
  609. this.contentBaseWatchers.push(watcher);
  610. };
  611. Server.prototype.invalidate = function () {
  612. if (this.middleware) this.middleware.invalidate();
  613. };
  614. // Export this logic, so that other implementations, like task-runners can use it
  615. Server.addDevServerEntrypoints = require('./util/addDevServerEntrypoints');
  616. module.exports = Server;