459 lines
14 KiB
JavaScript
459 lines
14 KiB
JavaScript
var fs = require("fs");
|
|
var path = require("path");
|
|
var webpackDevMiddleware = require("webpack-dev-middleware");
|
|
var express = require("express");
|
|
var compress = require("compression");
|
|
var sockjs = require("sockjs");
|
|
var StreamCache = require("stream-cache");
|
|
var http = require("http");
|
|
var https = require("https");
|
|
var httpProxyMiddleware = require("http-proxy-middleware");
|
|
var serveIndex = require("serve-index");
|
|
var historyApiFallback = require("connect-history-api-fallback");
|
|
|
|
function Server(compiler, options) {
|
|
// Default options
|
|
if(!options) options = {};
|
|
|
|
if(options.lazy && !options.filename) {
|
|
throw new Error("'filename' option must be set in lazy mode.");
|
|
}
|
|
|
|
this.hot = options.hot;
|
|
this.headers = options.headers;
|
|
this.clientLogLevel = options.clientLogLevel;
|
|
this.disableHostCheck = !!options.disableHostCheck;
|
|
this.publicHost = options.public;
|
|
this.sockets = [];
|
|
|
|
// Listening for events
|
|
var invalidPlugin = function() {
|
|
this.sockWrite(this.sockets, "invalid");
|
|
}.bind(this);
|
|
compiler.plugin("compile", invalidPlugin);
|
|
compiler.plugin("invalid", invalidPlugin);
|
|
compiler.plugin("done", function(stats) {
|
|
this._sendStats(this.sockets, stats.toJson());
|
|
this._stats = stats;
|
|
}.bind(this));
|
|
|
|
// Prepare live html page
|
|
var livePage = this.livePage = new StreamCache();
|
|
fs.createReadStream(path.join(__dirname, "..", "client", "live.html")).pipe(livePage);
|
|
|
|
// Prepare the live js file
|
|
var liveJs = new StreamCache();
|
|
fs.createReadStream(path.join(__dirname, "..", "client", "live.bundle.js")).pipe(liveJs);
|
|
|
|
// Prepare the inlined js file
|
|
var inlinedJs = new StreamCache();
|
|
fs.createReadStream(path.join(__dirname, "..", "client", "index.bundle.js")).pipe(inlinedJs);
|
|
|
|
// Init express server
|
|
var app = this.app = new express();
|
|
|
|
app.all("*", function(req, res, next) {
|
|
if(this.checkHost(req.headers))
|
|
return next();
|
|
res.send("Invalid Host header");
|
|
}.bind(this));
|
|
|
|
// middleware for serving webpack bundle
|
|
this.middleware = webpackDevMiddleware(compiler, options);
|
|
|
|
app.get("/__webpack_dev_server__/live.bundle.js", function(req, res) {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
liveJs.pipe(res);
|
|
});
|
|
|
|
app.get("/webpack-dev-server.js", function(req, res) {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
inlinedJs.pipe(res);
|
|
});
|
|
|
|
app.get("/webpack-dev-server/*", function(req, res) {
|
|
res.setHeader("Content-Type", "text/html");
|
|
this.livePage.pipe(res);
|
|
}.bind(this));
|
|
|
|
app.get("/webpack-dev-server", function(req, res) {
|
|
res.setHeader("Content-Type", "text/html");
|
|
res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
|
|
var path = this.middleware.getFilenameFromUrl(options.publicPath || "/");
|
|
var fs = this.middleware.fileSystem;
|
|
|
|
function writeDirectory(baseUrl, basePath) {
|
|
var content = fs.readdirSync(basePath);
|
|
res.write("<ul>");
|
|
content.forEach(function(item) {
|
|
var p = basePath + "/" + item;
|
|
if(fs.statSync(p).isFile()) {
|
|
res.write('<li><a href="');
|
|
res.write(baseUrl + item);
|
|
res.write('">');
|
|
res.write(item);
|
|
res.write('</a></li>');
|
|
if(/\.js$/.test(item)) {
|
|
var htmlItem = item.substr(0, item.length - 3);
|
|
res.write('<li><a href="');
|
|
res.write(baseUrl + htmlItem);
|
|
res.write('">');
|
|
res.write(htmlItem);
|
|
res.write('</a> (magic html for ');
|
|
res.write(item);
|
|
res.write(') (<a href="');
|
|
res.write(baseUrl.replace(/(^(https?:\/\/[^\/]+)?\/)/, "$1webpack-dev-server/") + htmlItem);
|
|
res.write('">webpack-dev-server</a>)</li>');
|
|
}
|
|
} else {
|
|
res.write('<li>');
|
|
res.write(item);
|
|
res.write('<br>');
|
|
writeDirectory(baseUrl + item + "/", p);
|
|
res.write('</li>');
|
|
}
|
|
});
|
|
res.write("</ul>");
|
|
}
|
|
writeDirectory(options.publicPath || "/", path);
|
|
res.end('</body></html>');
|
|
}.bind(this));
|
|
|
|
var features = {
|
|
compress: function() {
|
|
if(options.compress) {
|
|
// Enable gzip compression.
|
|
app.use(compress());
|
|
}
|
|
},
|
|
|
|
proxy: function() {
|
|
if(options.proxy) {
|
|
/**
|
|
* Assume a proxy configuration specified as:
|
|
* proxy: 'a url'
|
|
*/
|
|
if(typeof options.proxy === 'string') {
|
|
options.proxy = [{
|
|
context: options.proxy
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Assume a proxy configuration specified as:
|
|
* proxy: {
|
|
* 'context': { options }
|
|
* }
|
|
* OR
|
|
* proxy: {
|
|
* 'context': 'target'
|
|
* }
|
|
*/
|
|
if(!Array.isArray(options.proxy)) {
|
|
options.proxy = Object.keys(options.proxy).map(function(context) {
|
|
var proxyOptions;
|
|
// For backwards compatibility reasons.
|
|
var correctedContext = context.replace(/^\*$/, "**").replace(/\/\*$/, "");
|
|
|
|
if(typeof options.proxy[context] === 'string') {
|
|
proxyOptions = {
|
|
context: correctedContext,
|
|
target: options.proxy[context]
|
|
};
|
|
} else {
|
|
proxyOptions = options.proxy[context];
|
|
proxyOptions.context = correctedContext;
|
|
}
|
|
|
|
return proxyOptions;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assume a proxy configuration specified as:
|
|
* proxy: [
|
|
* {
|
|
* context: ...,
|
|
* ...options...
|
|
* }
|
|
* ]
|
|
*/
|
|
options.proxy.forEach(function(proxyConfig) {
|
|
var bypass = typeof proxyConfig.bypass === 'function';
|
|
var context = proxyConfig.context || proxyConfig.path;
|
|
var proxyMiddleware;
|
|
// 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) {
|
|
proxyMiddleware = httpProxyMiddleware(context, proxyConfig);
|
|
}
|
|
|
|
app.use(function(req, res, next) {
|
|
var bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
|
|
|
|
if(bypassUrl) {
|
|
req.url = bypassUrl;
|
|
next();
|
|
} else if(proxyMiddleware) {
|
|
return proxyMiddleware(req, res, next);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
historyApiFallback: function() {
|
|
if(options.historyApiFallback) {
|
|
// Fall back to /index.html if nothing else matches.
|
|
app.use(historyApiFallback(typeof options.historyApiFallback === 'object' ? options.historyApiFallback : null));
|
|
}
|
|
},
|
|
|
|
contentBase: function() {
|
|
if(options.contentBase !== false) {
|
|
var contentBase = options.contentBase || process.cwd();
|
|
|
|
if(Array.isArray(contentBase)) {
|
|
contentBase.forEach(function(item) {
|
|
app.get("*", express.static(item));
|
|
});
|
|
contentBase.forEach(function(item) {
|
|
app.get("*", serveIndex(item));
|
|
});
|
|
} else if(typeof contentBase === "object") {
|
|
console.log('Using contentBase as a proxy is deprecated and will be removed in the next major version. Please use the proxy option instead.\n\nTo update remove the contentBase option from webpack.config.js and add this:');
|
|
console.log('proxy: {\n\t"*": <your current contentBase configuration>\n}');
|
|
// Proxy every request to contentBase.target
|
|
app.all("*", function(req, res) {
|
|
proxy.web(req, res, contentBase, function(err) {
|
|
var msg = "cannot proxy to " + contentBase.target + " (" + err.message + ")";
|
|
this.sockWrite(this.sockets, "proxy-error", [msg]);
|
|
res.statusCode = 502;
|
|
res.end();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
} else if(/^(https?:)?\/\//.test(contentBase)) {
|
|
// Redirect every request to contentBase
|
|
app.get("*", function(req, res) {
|
|
res.writeHead(302, {
|
|
'Location': contentBase + req.path + (req._parsedUrl.search || "")
|
|
});
|
|
res.end();
|
|
});
|
|
} else if(typeof contentBase === "number") {
|
|
// Redirect every request to the port contentBase
|
|
app.get("*", function(req, res) {
|
|
res.writeHead(302, {
|
|
'Location': "//localhost:" + contentBase + req.path + (req._parsedUrl.search || "")
|
|
});
|
|
res.end();
|
|
});
|
|
} else {
|
|
// route content request
|
|
app.get("*", express.static(contentBase, options.staticOptions), serveIndex(contentBase));
|
|
}
|
|
}
|
|
}.bind(this),
|
|
|
|
middleware: function() {
|
|
// include our middleware to ensure it is able to handle '/index.html' request after redirect
|
|
app.use(this.middleware);
|
|
}.bind(this),
|
|
|
|
headers: function() {
|
|
app.all("*", this.setContentHeaders.bind(this));
|
|
}.bind(this),
|
|
|
|
magicHtml: function() {
|
|
app.get("*", this.serveMagicHtml.bind(this));
|
|
}.bind(this),
|
|
|
|
setup: function() {
|
|
if(typeof options.setup === "function")
|
|
options.setup(app, this);
|
|
}.bind(this)
|
|
};
|
|
|
|
var defaultFeatures = ["setup", "headers", "middleware"];
|
|
if(options.proxy)
|
|
defaultFeatures.push("proxy", "middleware");
|
|
if(options.historyApiFallback)
|
|
defaultFeatures.push("historyApiFallback", "middleware");
|
|
defaultFeatures.push("magicHtml");
|
|
if(options.contentBase !== false)
|
|
defaultFeatures.push("contentBase");
|
|
// compress is placed last and uses unshift so that it will be the first middleware used
|
|
if(options.compress)
|
|
defaultFeatures.unshift("compress");
|
|
|
|
(options.features || defaultFeatures).forEach(function(feature) {
|
|
features[feature]();
|
|
}, this);
|
|
|
|
if(options.https) {
|
|
// for keep supporting CLI parameters
|
|
if(typeof options.https === 'boolean') {
|
|
options.https = {
|
|
key: options.key,
|
|
cert: options.cert,
|
|
ca: options.ca,
|
|
pfx: options.pfx,
|
|
passphrase: options.pfxPassphrase
|
|
};
|
|
}
|
|
|
|
// using built-in self-signed certificate if no certificate was configured
|
|
options.https.key = options.https.key || fs.readFileSync(path.join(__dirname, "../ssl/server.key"));
|
|
options.https.cert = options.https.cert || fs.readFileSync(path.join(__dirname, "../ssl/server.crt"));
|
|
options.https.ca = options.https.ca || fs.readFileSync(path.join(__dirname, "../ssl/ca.crt"));
|
|
|
|
this.listeningApp = https.createServer(options.https, app);
|
|
} else {
|
|
this.listeningApp = http.createServer(app);
|
|
}
|
|
}
|
|
|
|
Server.prototype.use = function() {
|
|
this.app.use.apply(this.app, arguments);
|
|
}
|
|
|
|
Server.prototype.setContentHeaders = function(req, res, next) {
|
|
if(this.headers) {
|
|
for(var name in this.headers) {
|
|
res.setHeader(name, this.headers[name]);
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
Server.prototype.checkHost = function(headers) {
|
|
// allow user to opt-out this security check, at own risk
|
|
if(this.disableHostCheck) return true;
|
|
|
|
// get the Host header and extract hostname
|
|
// we don't care about port not matching
|
|
var hostHeader = headers.host;
|
|
if(!hostHeader) return false;
|
|
var idx = hostHeader.indexOf(":");
|
|
var hostname = idx >= 0 ? hostHeader.substr(0, idx) : hostHeader;
|
|
|
|
// always allow localhost host, for convience
|
|
if(hostname === "127.0.0.1" || hostname === "localhost") return true;
|
|
|
|
// allow hostname of listening adress
|
|
if(hostname === this.listenHostname) return true;
|
|
|
|
// also allow public hostname if provided
|
|
if(typeof this.publicHost === "string") {
|
|
var idxPublic = this.publicHost.indexOf(":");
|
|
var publicHostname = idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
|
|
if(hostname === publicHostname) return true;
|
|
}
|
|
|
|
// disallow
|
|
return false;
|
|
}
|
|
|
|
// delegate listen call and init sockjs
|
|
Server.prototype.listen = function(port, hostname) {
|
|
this.listenHostname = hostname;
|
|
this.listeningApp.listen.apply(this.listeningApp, arguments);
|
|
var sockServer = sockjs.createServer({
|
|
// Limit useless logs
|
|
log: function(severity, line) {
|
|
if(severity === "error") {
|
|
console.log(line);
|
|
}
|
|
}
|
|
});
|
|
sockServer.on("connection", function(conn) {
|
|
if(!conn) return;
|
|
if(!this.checkHost(conn.headers)) {
|
|
this.sockWrite([conn], "error", "Invalid Host header");
|
|
conn.close();
|
|
return;
|
|
}
|
|
this.sockets.push(conn);
|
|
|
|
conn.on("close", function() {
|
|
var connIndex = this.sockets.indexOf(conn);
|
|
if(connIndex >= 0) {
|
|
this.sockets.splice(connIndex, 1);
|
|
}
|
|
}.bind(this));
|
|
|
|
if(this.clientLogLevel)
|
|
this.sockWrite([conn], "log-level", this.clientLogLevel);
|
|
|
|
if(this.hot) this.sockWrite([conn], "hot");
|
|
|
|
if(!this._stats) return;
|
|
this._sendStats([conn], this._stats.toJson(), true);
|
|
}.bind(this));
|
|
|
|
sockServer.installHandlers(this.listeningApp, {
|
|
prefix: '/sockjs-node'
|
|
});
|
|
}
|
|
|
|
Server.prototype.close = function() {
|
|
this.sockets.forEach(function(sock) {
|
|
sock.close();
|
|
});
|
|
this.sockets = [];
|
|
this.middleware.close();
|
|
this.listeningApp.close();
|
|
}
|
|
|
|
Server.prototype.sockWrite = function(sockets, type, data) {
|
|
sockets.forEach(function(sock) {
|
|
sock.write(JSON.stringify({
|
|
type: type,
|
|
data: data
|
|
}));
|
|
});
|
|
}
|
|
|
|
Server.prototype.serveMagicHtml = function(req, res, next) {
|
|
var _path = req.path;
|
|
try {
|
|
if(!this.middleware.fileSystem.statSync(this.middleware.getFilenameFromUrl(_path + ".js")).isFile())
|
|
return next();
|
|
// Serve a page that executes the javascript
|
|
res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="');
|
|
res.write(_path);
|
|
res.write('.js');
|
|
res.write(req._parsedUrl.search || "");
|
|
res.end('"></script></body></html>');
|
|
} catch(e) {
|
|
return next();
|
|
}
|
|
}
|
|
|
|
// send stats to a socket or multiple sockets
|
|
Server.prototype._sendStats = function(sockets, stats, force) {
|
|
if(!force &&
|
|
stats &&
|
|
(!stats.errors || stats.errors.length === 0) &&
|
|
stats.assets &&
|
|
stats.assets.every(function(asset) {
|
|
return !asset.emitted;
|
|
})
|
|
)
|
|
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");
|
|
}
|
|
|
|
Server.prototype.invalidate = function() {
|
|
if(this.middleware) this.middleware.invalidate();
|
|
}
|
|
|
|
module.exports = Server;
|