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.

1037 lines
30 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'use strict';
/* eslint-disable
no-shadow,
no-undefined,
func-names
*/
const fs = require('fs');
const path = require('path');
const tls = require('tls');
const url = require('url');
const http = require('http');
const https = require('https');
const ip = require('ip');
const semver = require('semver');
const killable = require('killable');
const chokidar = require('chokidar');
const express = require('express');
const httpProxyMiddleware = require('http-proxy-middleware');
const historyApiFallback = require('connect-history-api-fallback');
const compress = require('compression');
const serveIndex = require('serve-index');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const validateOptions = require('schema-utils');
const isAbsoluteUrl = require('is-absolute-url');
const normalizeOptions = require('./utils/normalizeOptions');
const updateCompiler = require('./utils/updateCompiler');
const createLogger = require('./utils/createLogger');
const getCertificate = require('./utils/getCertificate');
const status = require('./utils/status');
const createDomain = require('./utils/createDomain');
const runBonjour = require('./utils/runBonjour');
const routes = require('./utils/routes');
const getSocketServerImplementation = require('./utils/getSocketServerImplementation');
const schema = require('./options.json');
// Workaround for node ^8.6.0, ^9.0.0
// DEFAULT_ECDH_CURVE is default to prime256v1 in these version
// breaking connection when certificate is not signed with prime256v1
// change it to auto allows OpenSSL to select the curve automatically
// See https://github.com/nodejs/node/issues/16196 for more information
if (semver.satisfies(process.version, '8.6.0 - 9')) {
tls.DEFAULT_ECDH_CURVE = 'auto';
}
if (!process.env.WEBPACK_DEV_SERVER) {
process.env.WEBPACK_DEV_SERVER = true;
}
class Server {
constructor(compiler, options = {}, _log) {
if (options.lazy && !options.filename) {
throw new Error("'filename' option must be set in lazy mode.");
}
validateOptions(schema, options, 'webpack Dev Server');
this.compiler = compiler;
this.options = options;
this.log = _log || createLogger(options);
if (this.options.transportMode !== undefined) {
this.log.warn(
'transportMode is an experimental option, meaning its usage could potentially change without warning'
);
}
normalizeOptions(this.compiler, this.options);
updateCompiler(this.compiler, this.options);
this.heartbeatInterval = 30000;
// this.SocketServerImplementation is a class, so it must be instantiated before use
this.socketServerImplementation = getSocketServerImplementation(
this.options
);
this.originalStats =
this.options.stats && Object.keys(this.options.stats).length
? this.options.stats
: {};
this.sockets = [];
this.contentBaseWatchers = [];
// TODO this.<property> is deprecated (remove them in next major release.) in favor this.options.<property>
this.hot = this.options.hot || this.options.hotOnly;
this.headers = this.options.headers;
this.progress = this.options.progress;
this.serveIndex = this.options.serveIndex;
this.clientOverlay = this.options.overlay;
this.clientLogLevel = this.options.clientLogLevel;
this.publicHost = this.options.public;
this.allowedHosts = this.options.allowedHosts;
this.disableHostCheck = !!this.options.disableHostCheck;
this.watchOptions = options.watchOptions || {};
// Replace leading and trailing slashes to normalize path
this.sockPath = `/${
this.options.sockPath
? this.options.sockPath.replace(/^\/|\/$/g, '')
: 'sockjs-node'
}`;
if (this.progress) {
this.setupProgressPlugin();
}
this.setupHooks();
this.setupApp();
this.setupCheckHostRoute();
this.setupDevMiddleware();
// set express routes
routes(this);
// Keep track of websocket proxies for external websocket upgrade.
this.websocketProxies = [];
this.setupFeatures();
this.setupHttps();
this.createServer();
killable(this.listeningApp);
// Proxy websockets without the initial http request
// https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
this.websocketProxies.forEach(function(wsProxy) {
this.listeningApp.on('upgrade', wsProxy.upgrade);
}, this);
}
setupProgressPlugin() {
// for CLI output
new webpack.ProgressPlugin({
profile: !!this.options.profile,
}).apply(this.compiler);
// for browser console output
new webpack.ProgressPlugin((percent, msg, addInfo) => {
percent = Math.floor(percent * 100);
if (percent === 100) {
msg = 'Compilation completed';
}
if (addInfo) {
msg = `${msg} (${addInfo})`;
}
this.sockWrite(this.sockets, 'progress-update', { percent, msg });
if (this.listeningApp) {
this.listeningApp.emit('progress-update', { percent, msg });
}
}).apply(this.compiler);
}
setupApp() {
// Init express server
// eslint-disable-next-line new-cap
this.app = new express();
}
setupHooks() {
// Listening for events
const invalidPlugin = () => {
this.sockWrite(this.sockets, 'invalid');
};
const addHooks = (compiler) => {
const { compile, invalid, done } = compiler.hooks;
compile.tap('webpack-dev-server', invalidPlugin);
invalid.tap('webpack-dev-server', invalidPlugin);
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
if (this.compiler.compilers) {
this.compiler.compilers.forEach(addHooks);
} else {
addHooks(this.compiler);
}
}
setupCheckHostRoute() {
this.app.all('*', (req, res, next) => {
if (this.checkHost(req.headers)) {
return next();
}
res.send('Invalid Host header');
});
}
setupDevMiddleware() {
// middleware for serving webpack bundle
this.middleware = webpackDevMiddleware(
this.compiler,
Object.assign({}, this.options, { logLevel: this.log.options.level })
);
}
setupCompressFeature() {
this.app.use(compress());
}
setupProxyFeature() {
/**
* Assume a proxy configuration specified as:
* proxy: {
* 'context': { options }
* }
* OR
* proxy: {
* 'context': 'target'
* }
*/
if (!Array.isArray(this.options.proxy)) {
if (Object.prototype.hasOwnProperty.call(this.options.proxy, 'target')) {
this.options.proxy = [this.options.proxy];
} else {
this.options.proxy = Object.keys(this.options.proxy).map((context) => {
let proxyOptions;
// For backwards compatibility reasons.
const correctedContext = context
.replace(/^\*$/, '**')
.replace(/\/\*$/, '');
if (typeof this.options.proxy[context] === 'string') {
proxyOptions = {
context: correctedContext,
target: this.options.proxy[context],
};
} else {
proxyOptions = Object.assign({}, this.options.proxy[context]);
proxyOptions.context = correctedContext;
}
proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
return proxyOptions;
});
}
}
const getProxyMiddleware = (proxyConfig) => {
const context = proxyConfig.context || proxyConfig.path;
// It is possible to use the `bypass` method without a `target`.
// However, the proxy middleware has no use in this case, and will fail to instantiate.
if (proxyConfig.target) {
return httpProxyMiddleware(context, proxyConfig);
}
};
/**
* Assume a proxy configuration specified as:
* proxy: [
* {
* context: ...,
* ...options...
* },
* // or:
* function() {
* return {
* context: ...,
* ...options...
* };
* }
* ]
*/
this.options.proxy.forEach((proxyConfigOrCallback) => {
let proxyMiddleware;
let proxyConfig =
typeof proxyConfigOrCallback === 'function'
? proxyConfigOrCallback()
: proxyConfigOrCallback;
proxyMiddleware = getProxyMiddleware(proxyConfig);
if (proxyConfig.ws) {
this.websocketProxies.push(proxyMiddleware);
}
const handle = (req, res, next) => {
if (typeof proxyConfigOrCallback === 'function') {
const newProxyConfig = proxyConfigOrCallback();
if (newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}
// - Check if we have a bypass function defined
// - In case the bypass function is defined we'll retrieve the
// bypassUrl from it otherwise bypassUrl would be null
const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
const bypassUrl = isByPassFuncDefined
? proxyConfig.bypass(req, res, proxyConfig)
: null;
if (typeof bypassUrl === 'boolean') {
// skip the proxy
req.url = null;
next();
} else if (typeof bypassUrl === 'string') {
// byPass to that url
req.url = bypassUrl;
next();
} else if (proxyMiddleware) {
return proxyMiddleware(req, res, next);
} else {
next();
}
};
this.app.use(handle);
// Also forward error requests to the proxy so it can handle them.
this.app.use((error, req, res, next) => handle(req, res, next));
});
}
setupHistoryApiFallbackFeature() {
const fallback =
typeof this.options.historyApiFallback === 'object'
? this.options.historyApiFallback
: null;
// Fall back to /index.html if nothing else matches.
this.app.use(historyApiFallback(fallback));
}
setupStaticFeature() {
const contentBase = this.options.contentBase;
const contentBasePublicPath = this.options.contentBasePublicPath;
if (Array.isArray(contentBase)) {
contentBase.forEach((item, index) => {
let publicPath = contentBasePublicPath;
if (
Array.isArray(contentBasePublicPath) &&
contentBasePublicPath[index]
) {
publicPath = contentBasePublicPath[index] || contentBasePublicPath[0];
}
this.app.use(publicPath, express.static(item));
});
} else if (isAbsoluteUrl(String(contentBase))) {
this.log.warn(
'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
);
this.log.warn(
'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
);
// Redirect every request to contentBase
this.app.get('*', (req, res) => {
res.writeHead(302, {
Location: contentBase + req.path + (req._parsedUrl.search || ''),
});
res.end();
});
} else if (typeof contentBase === 'number') {
this.log.warn(
'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
);
this.log.warn(
'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
);
// Redirect every request to the port contentBase
this.app.get('*', (req, res) => {
res.writeHead(302, {
Location: `//localhost:${contentBase}${req.path}${req._parsedUrl
.search || ''}`,
});
res.end();
});
} else {
// route content request
this.app.use(
contentBasePublicPath,
express.static(contentBase, this.options.staticOptions)
);
}
}
setupServeIndexFeature() {
const contentBase = this.options.contentBase;
const contentBasePublicPath = this.options.contentBasePublicPath;
if (Array.isArray(contentBase)) {
contentBase.forEach((item) => {
this.app.use(contentBasePublicPath, (req, res, next) => {
// serve-index doesn't fallthrough non-get/head request to next middleware
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next();
}
serveIndex(item, { icons: true })(req, res, next);
});
});
} else if (
typeof contentBase !== 'number' &&
!isAbsoluteUrl(String(contentBase))
) {
this.app.use(contentBasePublicPath, (req, res, next) => {
// serve-index doesn't fallthrough non-get/head request to next middleware
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next();
}
serveIndex(contentBase, { icons: true })(req, res, next);
});
}
}
setupWatchStaticFeature() {
const contentBase = this.options.contentBase;
if (isAbsoluteUrl(String(contentBase)) || typeof contentBase === 'number') {
throw new Error('Watching remote files is not supported.');
} else if (Array.isArray(contentBase)) {
contentBase.forEach((item) => {
if (isAbsoluteUrl(String(item)) || typeof item === 'number') {
throw new Error('Watching remote files is not supported.');
}
this._watch(item);
});
} else {
this._watch(contentBase);
}
}
setupBeforeFeature() {
// Todo rename onBeforeSetupMiddleware in next major release
// Todo pass only `this` argument
this.options.before(this.app, this, this.compiler);
}
setupMiddleware() {
this.app.use(this.middleware);
}
setupAfterFeature() {
// Todo rename onAfterSetupMiddleware in next major release
// Todo pass only `this` argument
this.options.after(this.app, this, this.compiler);
}
setupHeadersFeature() {
this.app.all('*', this.setContentHeaders.bind(this));
}
setupMagicHtmlFeature() {
this.app.get('*', this.serveMagicHtml.bind(this));
}
setupSetupFeature() {
this.log.warn(
'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`'
);
this.options.setup(this.app, this);
}
setupFeatures() {
const features = {
compress: () => {
if (this.options.compress) {
this.setupCompressFeature();
}
},
proxy: () => {
if (this.options.proxy) {
this.setupProxyFeature();
}
},
historyApiFallback: () => {
if (this.options.historyApiFallback) {
this.setupHistoryApiFallbackFeature();
}
},
// Todo rename to `static` in future major release
contentBaseFiles: () => {
this.setupStaticFeature();
},
// Todo rename to `serveIndex` in future major release
contentBaseIndex: () => {
this.setupServeIndexFeature();
},
// Todo rename to `watchStatic` in future major release
watchContentBase: () => {
this.setupWatchStaticFeature();
},
before: () => {
if (typeof this.options.before === 'function') {
this.setupBeforeFeature();
}
},
middleware: () => {
// include our middleware to ensure
// it is able to handle '/index.html' request after redirect
this.setupMiddleware();
},
after: () => {
if (typeof this.options.after === 'function') {
this.setupAfterFeature();
}
},
headers: () => {
this.setupHeadersFeature();
},
magicHtml: () => {
this.setupMagicHtmlFeature();
},
setup: () => {
if (typeof this.options.setup === 'function') {
this.setupSetupFeature();
}
},
};
const runnableFeatures = [];
// compress is placed last and uses unshift so that it will be the first middleware used
if (this.options.compress) {
runnableFeatures.push('compress');
}
runnableFeatures.push('setup', 'before', 'headers', 'middleware');
if (this.options.proxy) {
runnableFeatures.push('proxy', 'middleware');
}
if (this.options.contentBase !== false) {
runnableFeatures.push('contentBaseFiles');
}
if (this.options.historyApiFallback) {
runnableFeatures.push('historyApiFallback', 'middleware');
if (this.options.contentBase !== false) {
runnableFeatures.push('contentBaseFiles');
}
}
// checking if it's set to true or not set (Default : undefined => true)
this.serveIndex = this.serveIndex || this.serveIndex === undefined;
if (this.options.contentBase && this.serveIndex) {
runnableFeatures.push('contentBaseIndex');
}
if (this.options.watchContentBase) {
runnableFeatures.push('watchContentBase');
}
runnableFeatures.push('magicHtml');
if (this.options.after) {
runnableFeatures.push('after');
}
(this.options.features || runnableFeatures).forEach((feature) => {
features[feature]();
});
}
setupHttps() {
// if the user enables http2, we can safely enable https
if (this.options.http2 && !this.options.https) {
this.options.https = true;
}
if (this.options.https) {
// for keep supporting CLI parameters
if (typeof this.options.https === 'boolean') {
this.options.https = {
ca: this.options.ca,
pfx: this.options.pfx,
key: this.options.key,
cert: this.options.cert,
passphrase: this.options.pfxPassphrase,
requestCert: this.options.requestCert || false,
};
}
for (const property of ['ca', 'pfx', 'key', 'cert']) {
const value = this.options.https[property];
const isBuffer = value instanceof Buffer;
if (value && !isBuffer) {
let stats = null;
try {
stats = fs.lstatSync(fs.realpathSync(value)).isFile();
} catch (error) {
// ignore error
}
// It is file
this.options.https[property] = stats
? fs.readFileSync(path.resolve(value))
: value;
}
}
let fakeCert;
if (!this.options.https.key || !this.options.https.cert) {
fakeCert = getCertificate(this.log);
}
this.options.https.key = this.options.https.key || fakeCert;
this.options.https.cert = this.options.https.cert || fakeCert;
// note that options.spdy never existed. The user was able
// to set options.https.spdy before, though it was not in the
// docs. Keep options.https.spdy if the user sets it for
// backwards compatibility, but log a deprecation warning.
if (this.options.https.spdy) {
// for backwards compatibility: if options.https.spdy was passed in before,
// it was not altered in any way
this.log.warn(
'Providing custom spdy server options is deprecated and will be removed in the next major version.'
);
} else {
// if the normal https server gets this option, it will not affect it.
this.options.https.spdy = {
protocols: ['h2', 'http/1.1'],
};
}
}
}
createServer() {
if (this.options.https) {
// Only prevent HTTP/2 if http2 is explicitly set to false
const isHttp2 = this.options.http2 !== false;
// `spdy` is effectively unmaintained, and as a consequence of an
// implementation that extensively relies on Nodes non-public APIs, broken
// on Node 10 and above. In those cases, only https will be used for now.
// Once express supports Node's built-in HTTP/2 support, migrating over to
// that should be the best way to go.
// The relevant issues are:
// - https://github.com/nodejs/node/issues/21665
// - https://github.com/webpack/webpack-dev-server/issues/1449
// - https://github.com/expressjs/express/issues/3388
if (semver.gte(process.version, '10.0.0') || !isHttp2) {
if (this.options.http2) {
// the user explicitly requested http2 but is not getting it because
// of the node version.
this.log.warn(
'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it'
);
}
this.listeningApp = https.createServer(this.options.https, this.app);
} else {
// The relevant issues are:
// https://github.com/spdy-http2/node-spdy/issues/350
// https://github.com/webpack/webpack-dev-server/issues/1592
this.listeningApp = require('spdy').createServer(
this.options.https,
this.app
);
}
} else {
this.listeningApp = http.createServer(this.app);
}
this.listeningApp.on('error', (err) => {
this.log.error(err);
});
}
createSocketServer() {
const SocketServerImplementation = this.socketServerImplementation;
this.socketServer = new SocketServerImplementation(this);
this.socketServer.onConnection((connection, headers) => {
if (!connection) {
return;
}
if (!headers) {
this.log.warn(
'transportMode.server implementation must pass headers to the callback of onConnection(f) ' +
'via f(connection, headers) in order for clients to pass a headers security check'
);
}
if (!headers || !this.checkHost(headers) || !this.checkOrigin(headers)) {
this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
this.socketServer.close(connection);
return;
}
this.sockets.push(connection);
this.socketServer.onConnectionClose(connection, () => {
const idx = this.sockets.indexOf(connection);
if (idx >= 0) {
this.sockets.splice(idx, 1);
}
});
if (this.clientLogLevel) {
this.sockWrite([connection], 'log-level', this.clientLogLevel);
}
if (this.hot) {
this.sockWrite([connection], 'hot');
}
// TODO: change condition at major version
if (this.options.liveReload !== false) {
this.sockWrite([connection], 'liveReload', this.options.liveReload);
}
if (this.progress) {
this.sockWrite([connection], 'progress', this.progress);
}
if (this.clientOverlay) {
this.sockWrite([connection], 'overlay', this.clientOverlay);
}
if (!this._stats) {
return;
}
this._sendStats([connection], this.getStats(this._stats), true);
});
}
showStatus() {
const suffix =
this.options.inline !== false || this.options.lazy === true
? '/'
: '/webpack-dev-server/';
const uri = `${createDomain(this.options, this.listeningApp)}${suffix}`;
status(
uri,
this.options,
this.log,
this.options.stats && this.options.stats.colors
);
}
listen(port, hostname, fn) {
this.hostname = hostname;
return this.listeningApp.listen(port, hostname, (err) => {
this.createSocketServer();
if (this.options.bonjour) {
runBonjour(this.options);
}
this.showStatus();
if (fn) {
fn.call(this.listeningApp, err);
}
if (typeof this.options.onListening === 'function') {
this.options.onListening(this);
}
});
}
close(cb) {
this.sockets.forEach((socket) => {
this.socketServer.close(socket);
});
this.sockets = [];
this.contentBaseWatchers.forEach((watcher) => {
watcher.close();
});
this.contentBaseWatchers = [];
this.listeningApp.kill(() => {
this.middleware.close(cb);
});
}
static get DEFAULT_STATS() {
return {
all: false,
hash: true,
assets: true,
warnings: true,
errors: true,
errorDetails: false,
};
}
getStats(statsObj) {
const stats = Server.DEFAULT_STATS;
if (this.originalStats.warningsFilter) {
stats.warningsFilter = this.originalStats.warningsFilter;
}
return statsObj.toJson(stats);
}
use() {
// eslint-disable-next-line
this.app.use.apply(this.app, arguments);
}
setContentHeaders(req, res, next) {
if (this.headers) {
// eslint-disable-next-line
for (const name in this.headers) {
res.setHeader(name, this.headers[name]);
}
}
next();
}
checkHost(headers) {
return this.checkHeaders(headers, 'host');
}
checkOrigin(headers) {
return this.checkHeaders(headers, 'origin');
}
checkHeaders(headers, headerToCheck) {
// allow user to opt-out this security check, at own risk
if (this.disableHostCheck) {
return true;
}
if (!headerToCheck) {
headerToCheck = 'host';
}
// get the Host header and extract hostname
// we don't care about port not matching
const hostHeader = headers[headerToCheck];
if (!hostHeader) {
return false;
}
// use the node url-parser to retrieve the hostname from the host-header.
const hostname = url.parse(
// if hostHeader doesn't have scheme, add // for parsing.
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
false,
true
).hostname;
// always allow requests with explicit IPv4 or IPv6-address.
// A note on IPv6 addresses:
// hostHeader will always contain the brackets denoting
// an IPv6-address in URLs,
// these are removed from the hostname in url.parse(),
// so we have the pure IPv6-address in hostname.
// always allow localhost host, for convenience (hostname === 'localhost')
// allow hostname of listening address (hostname === this.hostname)
const isValidHostname =
ip.isV4Format(hostname) ||
ip.isV6Format(hostname) ||
hostname === 'localhost' ||
hostname === this.hostname;
if (isValidHostname) {
return true;
}
// always allow localhost host, for convenience
// allow if hostname is in allowedHosts
if (this.allowedHosts && this.allowedHosts.length) {
for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
const allowedHost = this.allowedHosts[hostIdx];
if (allowedHost === hostname) {
return true;
}
// support "." as a subdomain wildcard
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
if (allowedHost[0] === '.') {
// "example.com" (hostname === allowedHost.substring(1))
// "*.example.com" (hostname.endsWith(allowedHost))
if (
hostname === allowedHost.substring(1) ||
hostname.endsWith(allowedHost)
) {
return true;
}
}
}
}
// also allow public hostname if provided
if (typeof this.publicHost === 'string') {
const idxPublic = this.publicHost.indexOf(':');
const publicHostname =
idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
if (hostname === publicHostname) {
return true;
}
}
// disallow
return false;
}
// eslint-disable-next-line
sockWrite(sockets, type, data) {
sockets.forEach((socket) => {
this.socketServer.send(socket, JSON.stringify({ type, data }));
});
}
serveMagicHtml(req, res, next) {
const _path = req.path;
try {
const isFile = this.middleware.fileSystem
.statSync(this.middleware.getFilenameFromUrl(`${_path}.js`))
.isFile();
if (!isFile) {
return next();
}
// Serve a page that executes the javascript
const queries = req._parsedUrl.search || '';
const responsePage = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="${_path}.js${queries}"></script></body></html>`;
res.send(responsePage);
} catch (err) {
return next();
}
}
// send stats to a socket or multiple sockets
_sendStats(sockets, stats, force) {
const shouldEmit =
!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
stats.assets &&
stats.assets.every((asset) => !asset.emitted);
if (shouldEmit) {
return this.sockWrite(sockets, 'still-ok');
}
this.sockWrite(sockets, 'hash', stats.hash);
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');
}
}
_watch(watchPath) {
// duplicate the same massaging of options that watchpack performs
// https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
// this isn't an elegant solution, but we'll improve it in the future
const usePolling = this.watchOptions.poll ? true : undefined;
const interval =
typeof this.watchOptions.poll === 'number'
? this.watchOptions.poll
: undefined;
const watchOptions = {
ignoreInitial: true,
persistent: true,
followSymlinks: false,
atomic: false,
alwaysStat: true,
ignorePermissionErrors: true,
ignored: this.watchOptions.ignored,
usePolling,
interval,
};
const watcher = chokidar.watch(watchPath, watchOptions);
// disabling refreshing on changing the content
if (this.options.liveReload !== false) {
watcher.on('change', () => {
this.sockWrite(this.sockets, 'content-changed');
});
}
this.contentBaseWatchers.push(watcher);
}
invalidate(callback) {
if (this.middleware) {
this.middleware.invalidate(callback);
}
}
}
// Export this logic,
// so that other implementations,
// like task-runners can use it
Server.addDevServerEntrypoints = require('./utils/addEntries');
module.exports = Server;