first commit

This commit is contained in:
DanielRamirezGe
2020-01-14 20:43:08 -06:00
parent 3557894f7f
commit 5eecfbf6cd
26326 changed files with 2434803 additions and 0 deletions
+998
View File
@@ -0,0 +1,998 @@
'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.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.app, this.middleware, this.options);
// 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 });
}).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);
}
this.app.use((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();
}
});
});
}
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;
if (Array.isArray(contentBase)) {
contentBase.forEach((item) => {
this.app.get('*', 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.get(
'*',
express.static(contentBase, this.options.staticOptions)
);
}
}
setupServeIndexFeature() {
const contentBase = this.options.contentBase;
if (Array.isArray(contentBase)) {
contentBase.forEach((item) => {
this.app.get('*', serveIndex(item));
});
} else if (
typeof contentBase !== 'number' &&
!isAbsoluteUrl(String(contentBase))
) {
this.app.get('*', serveIndex(contentBase));
}
}
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);
}
}
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;
+481
View File
@@ -0,0 +1,481 @@
{
"type": "object",
"properties": {
"after": {
"instanceof": "Function"
},
"allowedHosts": {
"type": "array",
"items": {
"type": "string"
}
},
"before": {
"instanceof": "Function"
},
"bonjour": {
"type": "boolean"
},
"ca": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "Buffer"
}
]
},
"cert": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "Buffer"
}
]
},
"clientLogLevel": {
"enum": [
"info",
"warn",
"error",
"debug",
"trace",
"silent",
"none",
"warning"
]
},
"compress": {
"type": "boolean"
},
"contentBase": {
"anyOf": [
{
"enum": [false]
},
{
"type": "number"
},
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
]
},
"disableHostCheck": {
"type": "boolean"
},
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"filename": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "RegExp"
},
{
"instanceof": "Function"
}
]
},
"fs": {
"type": "object"
},
"headers": {
"type": "object"
},
"historyApiFallback": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "object"
}
]
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"hot": {
"type": "boolean"
},
"hotOnly": {
"type": "boolean"
},
"http2": {
"type": "boolean"
},
"https": {
"anyOf": [
{
"type": "object"
},
{
"type": "boolean"
}
]
},
"index": {
"type": "string"
},
"injectClient": {
"anyOf": [
{
"type": "boolean"
},
{
"instanceof": "Function"
}
]
},
"injectHot": {
"anyOf": [
{
"type": "boolean"
},
{
"instanceof": "Function"
}
]
},
"inline": {
"type": "boolean"
},
"key": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "Buffer"
}
]
},
"lazy": {
"type": "boolean"
},
"liveReload": {
"type": "boolean"
},
"log": {
"instanceof": "Function"
},
"logLevel": {
"enum": ["info", "warn", "error", "debug", "trace", "silent"]
},
"logTime": {
"type": "boolean"
},
"mimeTypes": {
"type": "object"
},
"noInfo": {
"type": "boolean"
},
"onListening": {
"instanceof": "Function"
},
"open": {
"anyOf": [
{
"type": "string"
},
{
"type": "boolean"
}
]
},
"openPage": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
]
},
"overlay": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"errors": {
"type": "boolean"
},
"warnings": {
"type": "boolean"
}
}
}
]
},
"pfx": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "Buffer"
}
]
},
"pfxPassphrase": {
"type": "string"
},
"port": {
"anyOf": [
{
"type": "number"
},
{
"type": "string"
},
{
"type": "null"
}
]
},
"profile": {
"type": "boolean"
},
"progress": {
"type": "boolean"
},
"proxy": {
"anyOf": [
{
"type": "object"
},
{
"type": "array",
"items": {
"anyOf": [
{
"type": "object"
},
{
"instanceof": "Function"
}
]
},
"minItems": 1
}
]
},
"public": {
"type": "string"
},
"publicPath": {
"type": "string"
},
"quiet": {
"type": "boolean"
},
"reporter": {
"instanceof": "Function"
},
"requestCert": {
"type": "boolean"
},
"serveIndex": {
"type": "boolean"
},
"serverSideRender": {
"type": "boolean"
},
"setup": {
"instanceof": "Function"
},
"sockHost": {
"type": "string"
},
"sockPath": {
"type": "string"
},
"sockPort": {
"anyOf": [
{
"type": "number"
},
{
"type": "string"
},
{
"type": "null"
}
]
},
"socket": {
"type": "string"
},
"staticOptions": {
"type": "object"
},
"stats": {
"anyOf": [
{
"type": "object"
},
{
"type": "boolean"
},
{
"enum": [
"none",
"errors-only",
"errors-warnings",
"minimal",
"normal",
"verbose"
]
}
]
},
"transportMode": {
"anyOf": [
{
"type": "object",
"properties": {
"client": {
"type": "string"
},
"server": {
"anyOf": [
{
"type": "string"
},
{
"instanceof": "Function"
}
]
}
},
"additionalProperties": false
},
{
"enum": ["sockjs", "ws"]
}
]
},
"useLocalIp": {
"type": "boolean"
},
"warn": {
"instanceof": "Function"
},
"watchContentBase": {
"type": "boolean"
},
"watchOptions": {
"type": "object"
},
"writeToDisk": {
"anyOf": [
{
"type": "boolean"
},
{
"instanceof": "Function"
}
]
}
},
"errorMessage": {
"properties": {
"after": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverafter)",
"allowedHosts": "should be {Array} (https://webpack.js.org/configuration/dev-server/#devserverallowedhosts)",
"before": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverbefore)",
"bonjour": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverbonjour)",
"ca": "should be {String|Buffer}",
"cert": "should be {String|Buffer}",
"clientLogLevel": "should be {String} and equal to one of the allowed values\n\n [ 'none', 'silent', 'info', 'debug', 'trace', 'error', 'warning', 'warn' ]\n\n (https://webpack.js.org/configuration/dev-server/#devserverclientloglevel)",
"compress": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devservercompress)",
"contentBase": "should be {Number|String|Array} (https://webpack.js.org/configuration/dev-server/#devservercontentbase)",
"disableHostCheck": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverdisablehostcheck)",
"features": "should be {Array}",
"filename": "should be {String|RegExp|Function} (https://webpack.js.org/configuration/dev-server/#devserverfilename-)",
"fs": "should be {Object} (https://github.com/webpack/webpack-dev-middleware#fs)",
"headers": "should be {Object} (https://webpack.js.org/configuration/dev-server/#devserverheaders-)",
"historyApiFallback": "should be {Boolean|Object} (https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback)",
"host": "should be {String|Null} (https://webpack.js.org/configuration/dev-server/#devserverhost)",
"hot": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhot)",
"hotOnly": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhotonly)",
"http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttp2)",
"https": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttps)",
"index": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverindex)",
"injectClient": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjectclient)",
"injectHot": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjecthot)",
"inline": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverinline)",
"key": "should be {String|Buffer}",
"lazy": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlazy-)",
"liveReload": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlivereload-)",
"log": "should be {Function}",
"logLevel": "should be {String} and equal to one of the allowed values\n\n [ 'info', 'warn', 'error', 'debug', 'trace', 'silent' ]\n\n (https://github.com/webpack/webpack-dev-middleware#loglevel)",
"logTime": "should be {Boolean} (https://github.com/webpack/webpack-dev-middleware#logtime)",
"mimeTypes": "should be {Object} (https://webpack.js.org/configuration/dev-server/#devservermimetypes-)",
"noInfo": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devservernoinfo-)",
"onListening": "should be {Function} (https://webpack.js.org/configuration/dev-server/#onlistening)",
"open": "should be {String|Boolean} (https://webpack.js.org/configuration/dev-server/#devserveropen)",
"openPage": "should be {String|Array} (https://webpack.js.org/configuration/dev-server/#devserveropenpage)",
"overlay": "should be {Boolean|Object} (https://webpack.js.org/configuration/dev-server/#devserveroverlay)",
"pfx": "should be {String|Buffer} (https://webpack.js.org/configuration/dev-server/#devserverpfx)",
"pfxPassphrase": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpfxpassphrase)",
"port": "should be {Number|String|Null} (https://webpack.js.org/configuration/dev-server/#devserverport)",
"profile": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverprofile)",
"progress": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverprogress---cli-only)",
"proxy": "should be {Object|Array} (https://webpack.js.org/configuration/dev-server/#devserverproxy)",
"public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpublic)",
"publicPath": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpublicpath-)",
"quiet": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverquiet-)",
"reporter": "should be {Function} (https://github.com/webpack/webpack-dev-middleware#reporter)",
"requestCert": "should be {Boolean}",
"serveIndex": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverserveindex)",
"serverSideRender": "should be {Boolean} (https://github.com/webpack/webpack-dev-middleware#serversiderender)",
"setup": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserversetup)",
"sockHost": "should be {String|Null} (https://webpack.js.org/configuration/dev-server/#devserversockhost)",
"sockPath": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserversockpath)",
"sockPort": "should be {Number|String|Null} (https://webpack.js.org/configuration/dev-server/#devserversockport)",
"socket": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserversocket)",
"staticOptions": "should be {Object} (https://webpack.js.org/configuration/dev-server/#devserverstaticoptions)",
"stats": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserverstats-)",
"transportMode": "should be {String|Object} (https://webpack.js.org/configuration/dev-server/#devservertransportmode)",
"useLocalIp": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserveruselocalip)",
"warn": "should be {Function}",
"watchContentBase": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverwatchcontentbase)",
"watchOptions": "should be {Object} (https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-)",
"writeToDisk": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverwritetodisk-)"
}
},
"additionalProperties": false
}
+9
View File
@@ -0,0 +1,9 @@
'use strict';
// base class that users should extend if they are making their own
// server implementation
module.exports = class BaseServer {
constructor(server) {
this.server = server;
}
};
+74
View File
@@ -0,0 +1,74 @@
'use strict';
/* eslint-disable
class-methods-use-this,
func-names
*/
const sockjs = require('sockjs');
const BaseServer = require('./BaseServer');
// Workaround for sockjs@~0.3.19
// sockjs will remove Origin header, however Origin header is required for checking host.
// See https://github.com/webpack/webpack-dev-server/issues/1604 for more information
{
const SockjsSession = require('sockjs/lib/transport').Session;
const decorateConnection = SockjsSession.prototype.decorateConnection;
SockjsSession.prototype.decorateConnection = function(req) {
decorateConnection.call(this, req);
const connection = this.connection;
if (
connection.headers &&
!('origin' in connection.headers) &&
'origin' in req.headers
) {
connection.headers.origin = req.headers.origin;
}
};
}
module.exports = class SockJSServer extends BaseServer {
// options has: error (function), debug (function), server (http/s server), path (string)
constructor(server) {
super(server);
this.socket = sockjs.createServer({
// Use provided up-to-date sockjs-client
sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
// Limit useless logs
log: (severity, line) => {
if (severity === 'error') {
this.server.log.error(line);
} else {
this.server.log.debug(line);
}
},
});
this.socket.installHandlers(this.server.listeningApp, {
prefix: this.server.sockPath,
});
}
send(connection, message) {
// prevent cases where the server is trying to send data while connection is closing
if (connection.readyState !== 1) {
return;
}
connection.write(message);
}
close(connection) {
connection.close();
}
// f should be passed the resulting connection and the connection headers
onConnection(f) {
this.socket.on('connection', (connection) => {
f(connection, connection ? connection.headers : null);
});
}
onConnectionClose(connection, f) {
connection.on('close', f);
}
};
+45
View File
@@ -0,0 +1,45 @@
'use strict';
/* eslint-disable
class-methods-use-this
*/
const ws = require('ws');
const BaseServer = require('./BaseServer');
module.exports = class WebsocketServer extends BaseServer {
constructor(server) {
super(server);
this.wsServer = new ws.Server({
server: this.server.listeningApp,
path: this.server.sockPath,
});
this.wsServer.on('error', (err) => {
this.server.log.error(err.message);
});
}
send(connection, message) {
// prevent cases where the server is trying to send data while connection is closing
if (connection.readyState !== 1) {
return;
}
connection.send(message);
}
close(connection) {
connection.close();
}
// f should be passed the resulting connection and the connection headers
onConnection(f) {
this.wsServer.on('connection', (connection, req) => {
f(connection, req.headers);
});
}
onConnectionClose(connection, f) {
connection.on('close', f);
}
};
+153
View File
@@ -0,0 +1,153 @@
'use strict';
const webpack = require('webpack');
const createDomain = require('./createDomain');
/**
* A Entry, it can be of type string or string[] or Object<string | string[],string>
* @typedef {(string[] | string | Object<string | string[],string>)} Entry
*/
/**
* Add entries Method
* @param {?Object} config - Webpack config
* @param {?Object} options - Dev-Server options
* @param {?Object} server
* @returns {void}
*/
function addEntries(config, options, server) {
if (options.inline !== false) {
// we're stubbing the app in this method as it's static and doesn't require
// a server to be supplied. createDomain requires an app with the
// address() signature.
const app = server || {
address() {
return { port: options.port };
},
};
/** @type {string} */
const domain = createDomain(options, app);
/** @type {string} */
const sockHost = options.sockHost ? `&sockHost=${options.sockHost}` : '';
/** @type {string} */
const sockPath = options.sockPath ? `&sockPath=${options.sockPath}` : '';
/** @type {string} */
const sockPort = options.sockPort ? `&sockPort=${options.sockPort}` : '';
/** @type {string} */
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;
/** @type {(string[] | string)} */
let hotEntry;
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
/**
* prependEntry Method
* @param {Entry} originalEntry
* @param {Entry} additionalEntries
* @returns {Entry}
*/
const prependEntry = (originalEntry, additionalEntries) => {
if (typeof originalEntry === 'function') {
return () =>
Promise.resolve(originalEntry()).then((entry) =>
prependEntry(entry, additionalEntries)
);
}
if (typeof originalEntry === 'object' && !Array.isArray(originalEntry)) {
/** @type {Object<string,string>} */
const clone = {};
Object.keys(originalEntry).forEach((key) => {
// entry[key] should be a string here
clone[key] = prependEntry(originalEntry[key], additionalEntries);
});
return clone;
}
// in this case, entry is a string or an array.
// make sure that we do not add duplicates.
/** @type {Entry} */
const entriesClone = additionalEntries.slice(0);
[].concat(originalEntry).forEach((newEntry) => {
if (!entriesClone.includes(newEntry)) {
entriesClone.push(newEntry);
}
});
return entriesClone;
};
/**
*
* Description of the option for checkInject method
* @typedef {Function} checkInjectOptionsParam
* @param {Object} _config - compilerConfig
* @return {Boolean}
*/
/**
*
* @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean
* @param {Object} _config
* @param {Boolean} defaultValue
* @return {Boolean}
*/
// eslint-disable-next-line no-shadow
const checkInject = (option, _config, defaultValue) => {
if (typeof option === 'boolean') return option;
if (typeof option === 'function') return option(_config);
return defaultValue;
};
// eslint-disable-next-line no-shadow
[].concat(config).forEach((config) => {
/** @type {Boolean} */
const webTarget = [
'web',
'webworker',
'electron-renderer',
'node-webkit',
undefined, // eslint-disable-line
null,
].includes(config.target);
/** @type {Entry} */
const additionalEntries = checkInject(
options.injectClient,
config,
webTarget
)
? [clientEntry]
: [];
if (hotEntry && checkInject(options.injectHot, config, true)) {
additionalEntries.push(hotEntry);
}
config.entry = prependEntry(config.entry || './src', additionalEntries);
if (options.hot || options.hotOnly) {
config.plugins = config.plugins || [];
if (
!config.plugins.find(
// Check for the name rather than the constructor reference in case
// there are multiple copies of webpack installed
(plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin'
)
) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
}
});
}
}
module.exports = addEntries;
+22
View File
@@ -0,0 +1,22 @@
'use strict';
const colors = {
info(useColor, msg) {
if (useColor) {
// Make text blue and bold, so it *pops*
return `\u001b[1m\u001b[34m${msg}\u001b[39m\u001b[22m`;
}
return msg;
},
error(useColor, msg) {
if (useColor) {
// Make text red and bold, so it *pops*
return `\u001b[1m\u001b[31m${msg}\u001b[39m\u001b[22m`;
}
return msg;
},
};
module.exports = colors;
+69
View File
@@ -0,0 +1,69 @@
'use strict';
const selfsigned = require('selfsigned');
function createCertificate(attributes) {
return selfsigned.generate(attributes, {
algorithm: 'sha256',
days: 30,
keySize: 2048,
extensions: [
{
name: 'basicConstraints',
cA: true,
},
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true,
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
timeStamping: true,
},
{
name: 'subjectAltName',
altNames: [
{
// type 2 is DNS
type: 2,
value: 'localhost',
},
{
type: 2,
value: 'localhost.localdomain',
},
{
type: 2,
value: 'lvh.me',
},
{
type: 2,
value: '*.lvh.me',
},
{
type: 2,
value: '[::1]',
},
{
// type 7 is IP
type: 7,
ip: '127.0.0.1',
},
{
type: 7,
ip: 'fe80::1',
},
],
},
],
});
}
module.exports = createCertificate;
+233
View File
@@ -0,0 +1,233 @@
'use strict';
const path = require('path');
const isAbsoluteUrl = require('is-absolute-url');
const defaultTo = require('./defaultTo');
function createConfig(config, argv, { port }) {
const firstWpOpt = Array.isArray(config) ? config[0] : config;
const options = firstWpOpt.devServer || {};
// This updates both config and firstWpOpt
firstWpOpt.mode = defaultTo(firstWpOpt.mode, 'development');
if (argv.bonjour) {
options.bonjour = true;
}
if (argv.host && (argv.host !== 'localhost' || !options.host)) {
options.host = argv.host;
}
if (argv.allowedHosts) {
options.allowedHosts = argv.allowedHosts.split(',');
}
if (argv.public) {
options.public = argv.public;
}
if (argv.socket) {
options.socket = argv.socket;
}
if (argv.sockHost) {
options.sockHost = argv.sockHost;
}
if (argv.sockPath) {
options.sockPath = argv.sockPath;
}
if (argv.sockPort) {
options.sockPort = argv.sockPort;
}
if (argv.liveReload === false) {
options.liveReload = false;
}
if (argv.profile) {
options.profile = argv.profile;
}
if (argv.progress) {
options.progress = argv.progress;
}
if (argv.overlay) {
options.overlay = argv.overlay;
}
if (!options.publicPath) {
// eslint-disable-next-line
options.publicPath =
(firstWpOpt.output && firstWpOpt.output.publicPath) || '';
if (
!isAbsoluteUrl(String(options.publicPath)) &&
options.publicPath[0] !== '/'
) {
options.publicPath = `/${options.publicPath}`;
}
}
if (!options.filename && firstWpOpt.output && firstWpOpt.output.filename) {
options.filename = firstWpOpt.output && firstWpOpt.output.filename;
}
if (!options.watchOptions && firstWpOpt.watchOptions) {
options.watchOptions = firstWpOpt.watchOptions;
}
if (argv.stdin) {
process.stdin.on('end', () => {
// eslint-disable-next-line no-process-exit
process.exit(0);
});
process.stdin.resume();
}
// TODO https://github.com/webpack/webpack-dev-server/issues/616 (v4)
// We should prefer CLI arg under config, now we always prefer `hot` from `devServer`
if (!options.hot) {
options.hot = argv.hot;
}
// TODO https://github.com/webpack/webpack-dev-server/issues/616 (v4)
// We should prefer CLI arg under config, now we always prefer `hotOnly` from `devServer`
if (!options.hotOnly) {
options.hotOnly = argv.hotOnly;
}
// TODO https://github.com/webpack/webpack-dev-server/issues/616 (v4)
// We should prefer CLI arg under config, now we always prefer `clientLogLevel` from `devServer`
if (!options.clientLogLevel && argv.clientLogLevel) {
options.clientLogLevel = argv.clientLogLevel;
}
if (argv.contentBase) {
options.contentBase = argv.contentBase;
if (Array.isArray(options.contentBase)) {
options.contentBase = options.contentBase.map((p) => path.resolve(p));
} else if (/^[0-9]$/.test(options.contentBase)) {
options.contentBase = +options.contentBase;
} else if (!isAbsoluteUrl(String(options.contentBase))) {
options.contentBase = path.resolve(options.contentBase);
}
}
// It is possible to disable the contentBase by using
// `--no-content-base`, which results in arg["content-base"] = false
else if (argv.contentBase === false) {
options.contentBase = false;
}
if (argv.watchContentBase) {
options.watchContentBase = true;
}
if (!options.stats) {
options.stats = defaultTo(firstWpOpt.stats, {
cached: false,
cachedAssets: false,
});
}
if (
typeof options.stats === 'object' &&
typeof options.stats.colors === 'undefined' &&
argv.color
) {
options.stats = Object.assign({}, options.stats, { colors: argv.color });
}
if (argv.lazy) {
options.lazy = true;
}
// TODO remove in `v4`
if (!argv.info) {
options.noInfo = true;
}
// TODO remove in `v4`
if (argv.quiet) {
options.quiet = true;
}
if (argv.https) {
options.https = true;
}
if (argv.http2) {
options.http2 = true;
}
if (argv.key) {
options.key = argv.key;
}
if (argv.cert) {
options.cert = argv.cert;
}
if (argv.cacert) {
options.ca = argv.cacert;
}
if (argv.pfx) {
options.pfx = argv.pfx;
}
if (argv.pfxPassphrase) {
options.pfxPassphrase = argv.pfxPassphrase;
}
if (argv.inline === false) {
options.inline = false;
}
if (argv.historyApiFallback) {
options.historyApiFallback = true;
}
if (argv.compress) {
options.compress = true;
}
if (argv.disableHostCheck) {
options.disableHostCheck = true;
}
if (argv.openPage) {
options.open = true;
options.openPage = argv.openPage.split(',');
}
if (typeof argv.open !== 'undefined') {
options.open = argv.open !== '' ? argv.open : true;
}
if (options.open && !options.openPage) {
options.openPage = '';
}
if (argv.useLocalIp) {
options.useLocalIp = true;
}
// Kind of weird, but ensures prior behavior isn't broken in cases
// that wouldn't throw errors. E.g. both argv.port and options.port
// were specified, but since argv.port is 8080, options.port will be
// tried first instead.
options.port =
argv.port === port
? defaultTo(options.port, argv.port)
: defaultTo(argv.port, options.port);
return options;
}
module.exports = createConfig;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const url = require('url');
const ip = require('internal-ip');
function createDomain(options, server) {
const protocol = options.https ? 'https' : 'http';
const hostname = options.useLocalIp
? ip.v4.sync() || 'localhost'
: options.host || 'localhost';
// eslint-disable-next-line no-nested-ternary
const port = options.socket ? 0 : server ? server.address().port : 0;
// use explicitly defined public url
// (prefix with protocol if not explicitly given)
if (options.public) {
return /^[a-zA-Z]+:\/\//.test(options.public)
? `${options.public}`
: `${protocol}://${options.public}`;
}
// the formatted domain (url without path) of the webpack server
return url.format({
protocol,
hostname,
port,
});
}
module.exports = createDomain;
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const log = require('webpack-log');
function createLogger(options = {}) {
let level = options.logLevel || 'info';
if (options.noInfo === true) {
level = 'warn';
}
if (options.quiet === true) {
level = 'silent';
}
return log({
name: 'wds',
level,
timestamp: options.logTime,
});
}
module.exports = createLogger;
+3
View File
@@ -0,0 +1,3 @@
'use strict';
module.exports = 8080;
+7
View File
@@ -0,0 +1,7 @@
'use strict';
function defaultTo(value, def) {
return value == null ? def : value;
}
module.exports = defaultTo;
+39
View File
@@ -0,0 +1,39 @@
'use strict';
const pRetry = require('p-retry');
const portfinder = require('portfinder');
const defaultPort = require('./defaultPort');
const defaultTo = require('./defaultTo');
const tryParseInt = require('./tryParseInt');
function runPortFinder() {
return new Promise((resolve, reject) => {
portfinder.basePort = defaultPort;
portfinder.getPort((error, port) => {
if (error) {
return reject(error);
}
return resolve(port);
});
});
}
function findPort(port) {
if (port) {
return Promise.resolve(port);
}
// Try to find unused port and listen on it for 3 times,
// if port is not specified in options.
// Because NaN == null is false, defaultTo fails if parseInt returns NaN
// so the tryParseInt function is introduced to handle NaN
const defaultPortRetry = defaultTo(
tryParseInt(process.env.DEFAULT_PORT_RETRY),
3
);
return pRetry(runPortFinder, { retries: defaultPortRetry });
}
module.exports = findPort;
+45
View File
@@ -0,0 +1,45 @@
'use strict';
const path = require('path');
const fs = require('fs');
const del = require('del');
const createCertificate = require('./createCertificate');
function getCertificate(logger) {
// Use a self-signed certificate if no certificate was configured.
// Cycle certs every 24 hours
const certificatePath = path.join(__dirname, '../../ssl/server.pem');
let certificateExists = fs.existsSync(certificatePath);
if (certificateExists) {
const certificateTtl = 1000 * 60 * 60 * 24;
const certificateStat = fs.statSync(certificatePath);
const now = new Date();
// cert is more than 30 days old, kill it with fire
if ((now - certificateStat.ctime) / certificateTtl > 30) {
logger.info('SSL Certificate is more than 30 days old. Removing.');
del.sync([certificatePath], { force: true });
certificateExists = false;
}
}
if (!certificateExists) {
logger.info('Generating SSL Certificate');
const attributes = [{ name: 'commonName', value: 'localhost' }];
const pems = createCertificate(attributes);
fs.writeFileSync(certificatePath, pems.private + pems.cert, {
encoding: 'utf8',
});
}
return fs.readFileSync(certificatePath);
}
module.exports = getCertificate;
+37
View File
@@ -0,0 +1,37 @@
'use strict';
function getSocketClientPath(options) {
let ClientImplementation;
let clientImplFound = true;
switch (typeof options.transportMode.client) {
case 'string':
// could be 'sockjs', 'ws', or a path that should be required
if (options.transportMode.client === 'sockjs') {
ClientImplementation = require('../../client/clients/SockJSClient');
} else if (options.transportMode.client === 'ws') {
ClientImplementation = require('../../client/clients/WebsocketClient');
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
ClientImplementation = require(options.transportMode.client);
} catch (e) {
clientImplFound = false;
}
}
break;
default:
clientImplFound = false;
}
if (!clientImplFound) {
throw new Error(
"transportMode.client must be a string denoting a default implementation (e.g. 'sockjs', 'ws') or a full path to " +
'a JS file which exports a class extending BaseClient (webpack-dev-server/client-src/clients/BaseClient) ' +
'via require.resolve(...)'
);
}
return ClientImplementation.getClientPath(options);
}
module.exports = getSocketClientPath;
@@ -0,0 +1,42 @@
'use strict';
function getSocketServerImplementation(options) {
let ServerImplementation;
let serverImplFound = true;
switch (typeof options.transportMode.server) {
case 'string':
// could be 'sockjs', in the future 'ws', or a path that should be required
if (options.transportMode.server === 'sockjs') {
ServerImplementation = require('../servers/SockJSServer');
} else if (options.transportMode.server === 'ws') {
ServerImplementation = require('../servers/WebsocketServer');
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
ServerImplementation = require(options.transportMode.server);
} catch (e) {
serverImplFound = false;
}
}
break;
case 'function':
// potentially do more checks here to confirm that the user implemented this properlly
// since errors could be difficult to understand
ServerImplementation = options.transportMode.server;
break;
default:
serverImplFound = false;
}
if (!serverImplFound) {
throw new Error(
"transportMode.server must be a string denoting a default implementation (e.g. 'sockjs', 'ws'), a full path to " +
'a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer) ' +
'via require.resolve(...), or the class itself which extends BaseServer'
);
}
return ServerImplementation;
}
module.exports = getSocketServerImplementation;
+10
View File
@@ -0,0 +1,10 @@
'use strict';
function getVersions() {
return (
`webpack-dev-server ${require('../../package.json').version}\n` +
`webpack ${require('webpack/package.json').version}`
);
}
module.exports = getVersions;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/* eslint-disable
no-undefined
*/
function normalizeOptions(compiler, options) {
// Setup default value
options.contentBase =
options.contentBase !== undefined ? options.contentBase : process.cwd();
// normalize transportMode option
if (options.transportMode === undefined) {
options.transportMode = {
server: 'sockjs',
client: 'sockjs',
};
} else {
switch (typeof options.transportMode) {
case 'string':
options.transportMode = {
server: options.transportMode,
client: options.transportMode,
};
break;
// if not a string, it is an object
default:
options.transportMode.server = options.transportMode.server || 'sockjs';
options.transportMode.client = options.transportMode.client || 'sockjs';
}
}
if (!options.watchOptions) {
options.watchOptions = {};
}
}
module.exports = normalizeOptions;
+44
View File
@@ -0,0 +1,44 @@
'use strict';
const createConfig = require('./createConfig');
const defaultPort = require('./defaultPort');
const findPort = require('./findPort');
function processOptions(config, argv, callback) {
// processOptions {Promise}
if (typeof config.then === 'function') {
config
.then((conf) => processOptions(conf, argv, callback))
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err.stack || err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
return;
}
// Taken out of yargs because we must know if
// it wasn't given by the user, in which case
// we should use portfinder.
const options = createConfig(config, argv, { port: defaultPort });
if (options.socket) {
callback(config, options);
} else {
findPort(options.port)
.then((port) => {
options.port = port;
callback(config, options);
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err.stack || err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
}
}
module.exports = processOptions;
+89
View File
@@ -0,0 +1,89 @@
'use strict';
const { createReadStream } = require('fs');
const { join } = require('path');
const clientBasePath = join(__dirname, '..', '..', 'client');
function routes(app, middleware, options) {
app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
createReadStream(join(clientBasePath, 'live.bundle.js')).pipe(res);
});
app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
createReadStream(join(clientBasePath, 'sockjs.bundle.js')).pipe(res);
});
app.get('/webpack-dev-server.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
createReadStream(join(clientBasePath, 'index.bundle.js')).pipe(res);
});
app.get('/webpack-dev-server/*', (req, res) => {
res.setHeader('Content-Type', 'text/html');
createReadStream(join(clientBasePath, 'live.html')).pipe(res);
});
app.get('/webpack-dev-server', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.write(
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
);
const outputPath = middleware.getFilenameFromUrl(options.publicPath || '/');
const filesystem = middleware.fileSystem;
writeDirectory(options.publicPath || '/', outputPath);
res.end('</body></html>');
function writeDirectory(baseUrl, basePath) {
const content = filesystem.readdirSync(basePath);
res.write('<ul>');
content.forEach((item) => {
const p = `${basePath}/${item}`;
if (filesystem.statSync(p).isFile()) {
res.write(`<li><a href="${baseUrl + item}">${item}</a></li>`);
if (/\.js$/.test(item)) {
const html = item.substr(0, item.length - 3);
const containerHref = baseUrl + html;
const magicHtmlHref =
baseUrl.replace(
// eslint-disable-next-line
/(^(https?:\/\/[^\/]+)?\/)/,
'$1webpack-dev-server/'
) + html;
res.write(
`<li><a href="${containerHref}">${html}</a>` +
` (magic html for ${item}) (<a href="${magicHtmlHref}">webpack-dev-server</a>)` +
`</li>`
);
}
} else {
res.write(`<li>${item}<br>`);
writeDirectory(`${baseUrl + item}/`, p);
res.write('</li>');
}
});
res.write('</ul>');
}
});
}
module.exports = routes;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
function runBonjour({ port }) {
const bonjour = require('bonjour')();
const os = require('os');
bonjour.publish({
name: `Webpack Dev Server ${os.hostname()}:${port}`,
port,
type: 'http',
subtypes: ['webpack'],
});
process.on('exit', () => {
bonjour.unpublishAll(() => {
bonjour.destroy();
});
});
}
module.exports = runBonjour;
+34
View File
@@ -0,0 +1,34 @@
'use strict';
const open = require('opn');
const isAbsoluteUrl = require('is-absolute-url');
function runOpen(uri, options, log) {
// https://github.com/webpack/webpack-dev-server/issues/1990
let openOptions = { wait: false };
let openOptionValue = '';
if (typeof options.open === 'string') {
openOptions = Object.assign({}, openOptions, { app: options.open });
openOptionValue = `: "${options.open}"`;
}
const pages =
typeof options.openPage === 'string'
? [options.openPage]
: options.openPage || [''];
return Promise.all(
pages.map((page) => {
const pageUrl = page && isAbsoluteUrl(page) ? page : `${uri}${page}`;
return open(pageUrl, openOptions).catch(() => {
log.warn(
`Unable to open "${pageUrl}" in browser${openOptionValue}. If you are running in a headless environment, please do not use the --open flag`
);
});
})
);
}
module.exports = runOpen;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const signals = ['SIGINT', 'SIGTERM'];
function setupExitSignals(serverData) {
signals.forEach((signal) => {
process.on(signal, () => {
if (serverData.server) {
serverData.server.close(() => {
// eslint-disable-next-line no-process-exit
process.exit();
});
} else {
// eslint-disable-next-line no-process-exit
process.exit();
}
});
});
}
module.exports = setupExitSignals;
+61
View File
@@ -0,0 +1,61 @@
'use strict';
const logger = require('webpack-log');
const colors = require('./colors');
const runOpen = require('./runOpen');
// TODO: don't emit logs when webpack-dev-server is used via Node.js API
function status(uri, options, log, useColor) {
if (options.quiet === true) {
// Add temporary logger to output just the status of the dev server
log = logger({
name: 'wds',
level: 'info',
timestamp: options.logTime,
});
}
const contentBase = Array.isArray(options.contentBase)
? options.contentBase.join(', ')
: options.contentBase;
if (options.socket) {
log.info(`Listening to socket at ${colors.info(useColor, options.socket)}`);
} else {
log.info(`Project is running at ${colors.info(useColor, uri)}`);
}
log.info(
`webpack output is served from ${colors.info(useColor, options.publicPath)}`
);
if (contentBase) {
log.info(
`Content not from webpack is served from ${colors.info(
useColor,
contentBase
)}`
);
}
if (options.historyApiFallback) {
log.info(
`404s will fallback to ${colors.info(
useColor,
options.historyApiFallback.index || '/index.html'
)}`
);
}
if (options.bonjour) {
log.info(
'Broadcasting "http" with subtype of "webpack" via ZeroConf DNS (Bonjour)'
);
}
if (options.open) {
runOpen(uri, options, log);
}
}
module.exports = status;
+13
View File
@@ -0,0 +1,13 @@
'use strict';
function tryParseInt(input) {
const output = parseInt(input, 10);
if (Number.isNaN(output)) {
return null;
}
return output;
}
module.exports = tryParseInt;
+73
View File
@@ -0,0 +1,73 @@
'use strict';
/* eslint-disable
no-shadow,
no-undefined
*/
const webpack = require('webpack');
const addEntries = require('./addEntries');
const getSocketClientPath = require('./getSocketClientPath');
function updateCompiler(compiler, options) {
if (options.inline !== false) {
const findHMRPlugin = (config) => {
if (!config.plugins) {
return undefined;
}
return config.plugins.find(
(plugin) => plugin.constructor === webpack.HotModuleReplacementPlugin
);
};
const compilers = [];
const compilersWithoutHMR = [];
let webpackConfig;
if (compiler.compilers) {
webpackConfig = [];
compiler.compilers.forEach((compiler) => {
webpackConfig.push(compiler.options);
compilers.push(compiler);
if (!findHMRPlugin(compiler.options)) {
compilersWithoutHMR.push(compiler);
}
});
} else {
webpackConfig = compiler.options;
compilers.push(compiler);
if (!findHMRPlugin(compiler.options)) {
compilersWithoutHMR.push(compiler);
}
}
// it's possible that we should clone the config before doing
// this, but it seems safe not to since it actually reflects
// the changes we are making to the compiler
// important: this relies on the fact that addEntries now
// prevents duplicate new entries.
addEntries(webpackConfig, options);
compilers.forEach((compiler) => {
const config = compiler.options;
compiler.hooks.entryOption.call(config.context, config.entry);
const providePlugin = new webpack.ProvidePlugin({
__webpack_dev_server_client__: getSocketClientPath(options),
});
providePlugin.apply(compiler);
});
// do not apply the plugin unless it didn't exist before.
if (options.hot || options.hotOnly) {
compilersWithoutHMR.forEach((compiler) => {
// addDevServerEntrypoints above should have added the plugin
// to the compiler options
const plugin = findHMRPlugin(compiler.options);
if (plugin) {
plugin.apply(compiler);
}
});
}
}
}
module.exports = updateCompiler;