You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Server.js 23 KiB

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