2017-09-13 07:52:34 +02:00

431 lines
11 KiB
JavaScript
Vendored

#!/usr/bin/env node
"use strict";
const path = require("path");
const open = require("opn");
const fs = require("fs");
const net = require("net");
const portfinder = require("portfinder");
const addDevServerEntrypoints = require("../lib/util/addDevServerEntrypoints");
const createDomain = require("../lib/util/createDomain");
// Local version replaces global one
try {
const localWebpackDevServer = require.resolve(path.join(process.cwd(), "node_modules", "webpack-dev-server", "bin", "webpack-dev-server.js"));
if(__filename !== localWebpackDevServer) {
return require(localWebpackDevServer);
}
} catch(e) {}
const Server = require("../lib/Server");
const webpack = require("webpack");
function versionInfo() {
return `webpack-dev-server ${require("../package.json").version}\n` +
`webpack ${require("webpack/package.json").version}`;
}
function colorInfo(useColor, msg) {
if(useColor)
// Make text blue and bold, so it *pops*
return `\u001b[1m\u001b[34m${msg}\u001b[39m\u001b[22m`;
return msg;
}
function colorError(useColor, msg) {
if(useColor)
// Make text red and bold, so it *pops*
return `\u001b[1m\u001b[31m${msg}\u001b[39m\u001b[22m`;
return msg;
}
const yargs = require("yargs")
.usage(`${versionInfo()
}\nUsage: https://webpack.js.org/configuration/dev-server/`);
require("webpack/bin/config-yargs")(yargs);
// It is important that this is done after the webpack yargs config,
// so it overrides webpack's version info.
yargs.version(versionInfo);
const ADVANCED_GROUP = "Advanced options:";
const DISPLAY_GROUP = "Stats options:";
const SSL_GROUP = "SSL options:";
const CONNECTION_GROUP = "Connection options:";
const RESPONSE_GROUP = "Response options:";
const BASIC_GROUP = "Basic options:";
// Taken out of yargs because we must know if
// it wasn't given by the user, in which case
// we should use portfinder.
const DEFAULT_PORT = 8080;
yargs.options({
"lazy": {
type: "boolean",
describe: "Lazy"
},
"inline": {
type: "boolean",
default: true,
describe: "Inline mode (set to false to disable including client scripts like livereload)"
},
"progress": {
type: "boolean",
describe: "Print compilation progress in percentage",
group: BASIC_GROUP
},
"hot-only": {
type: "boolean",
describe: "Do not refresh page if HMR fails",
group: ADVANCED_GROUP
},
"stdin": {
type: "boolean",
describe: "close when stdin ends"
},
"open": {
type: "boolean",
describe: "Open default browser"
},
"color": {
type: "boolean",
alias: "colors",
default: function supportsColor() {
return require("supports-color");
},
group: DISPLAY_GROUP,
describe: "Enables/Disables colors on the console"
},
"info": {
type: "boolean",
group: DISPLAY_GROUP,
default: true,
describe: "Info"
},
"quiet": {
type: "boolean",
group: DISPLAY_GROUP,
describe: "Quiet"
},
"client-log-level": {
type: "string",
group: DISPLAY_GROUP,
default: "info",
describe: "Log level in the browser (info, warning, error or none)"
},
"https": {
type: "boolean",
group: SSL_GROUP,
describe: "HTTPS"
},
"key": {
type: "string",
describe: "Path to a SSL key.",
group: SSL_GROUP
},
"cert": {
type: "string",
describe: "Path to a SSL certificate.",
group: SSL_GROUP
},
"cacert": {
type: "string",
describe: "Path to a SSL CA certificate.",
group: SSL_GROUP
},
"pfx": {
type: "string",
describe: "Path to a SSL pfx file.",
group: SSL_GROUP
},
"pfx-passphrase": {
type: "string",
describe: "Passphrase for pfx file.",
group: SSL_GROUP
},
"content-base": {
type: "string",
describe: "A directory or URL to serve HTML content from.",
group: RESPONSE_GROUP
},
"watch-content-base": {
type: "boolean",
describe: "Enable live-reloading of the content-base.",
group: RESPONSE_GROUP
},
"history-api-fallback": {
type: "boolean",
describe: "Fallback to /index.html for Single Page Applications.",
group: RESPONSE_GROUP
},
"compress": {
type: "boolean",
describe: "Enable gzip compression",
group: RESPONSE_GROUP
},
"port": {
describe: "The port",
group: CONNECTION_GROUP
},
"socket": {
type: "String",
describe: "Socket to listen",
group: CONNECTION_GROUP
},
"public": {
type: "string",
describe: "The public hostname/ip address of the server",
group: CONNECTION_GROUP
},
"host": {
type: "string",
default: "localhost",
describe: "The hostname/ip address the server will bind to",
group: CONNECTION_GROUP
}
});
const argv = yargs.argv;
const wpOpt = require("webpack/bin/convert-argv")(yargs, argv, {
outputFilename: "/bundle.js"
});
function processOptions(wpOpt) {
// process Promise
if(typeof wpOpt.then === "function") {
wpOpt.then(processOptions).catch(function(err) {
console.error(err.stack || err);
process.exit(); // eslint-disable-line
});
return;
}
const firstWpOpt = Array.isArray(wpOpt) ? wpOpt[0] : wpOpt;
const options = wpOpt.devServer || firstWpOpt.devServer || {};
if(argv.host !== "localhost" || !options.host)
options.host = argv.host;
if(argv.public)
options.public = argv.public;
if(argv.socket)
options.socket = argv.socket;
if(!options.publicPath) {
options.publicPath = firstWpOpt.output && firstWpOpt.output.publicPath || "";
if(!/^(https?:)?\/\//.test(options.publicPath) && options.publicPath[0] !== "/")
options.publicPath = `/${options.publicPath}`;
}
if(!options.filename)
options.filename = firstWpOpt.output && firstWpOpt.output.filename;
if(!options.watchOptions)
options.watchOptions = firstWpOpt.watchOptions;
if(argv["stdin"]) {
process.stdin.on("end", function() {
process.exit(0); // eslint-disable-line no-process-exit
});
process.stdin.resume();
}
if(!options.hot)
options.hot = argv["hot"];
if(!options.hotOnly)
options.hotOnly = argv["hot-only"];
if(!options.clientLogLevel)
options.clientLogLevel = argv["client-log-level"];
if(options.contentBase === undefined) {
if(argv["content-base"]) {
options.contentBase = argv["content-base"];
if(Array.isArray(options.contentBase)) {
options.contentBase = options.contentBase.map(function(val) {
return path.resolve(val);
});
} else if(/^[0-9]$/.test(options.contentBase))
options.contentBase = +options.contentBase;
else if(!/^(https?:)?\/\//.test(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["content-base"] === false) {
options.contentBase = false;
}
}
if(argv["watch-content-base"])
options.watchContentBase = true;
if(!options.stats) {
options.stats = {
cached: false,
cachedAssets: false
};
}
if(typeof options.stats === "object" && typeof options.stats.colors === "undefined")
options.stats.colors = argv.color;
if(argv["lazy"])
options.lazy = true;
if(!argv["info"])
options.noInfo = true;
if(argv["quiet"])
options.quiet = true;
if(argv["https"])
options.https = true;
if(argv["cert"])
options.cert = fs.readFileSync(path.resolve(argv["cert"]));
if(argv["key"])
options.key = fs.readFileSync(path.resolve(argv["key"]));
if(argv["cacert"])
options.ca = fs.readFileSync(path.resolve(argv["cacert"]));
if(argv["pfx"])
options.pfx = fs.readFileSync(path.resolve(argv["pfx"]));
if(argv["pfx-passphrase"])
options.pfxPassphrase = argv["pfx-passphrase"];
if(argv["inline"] === false)
options.inline = false;
if(argv["history-api-fallback"])
options.historyApiFallback = true;
if(argv["compress"])
options.compress = true;
if(argv["open"])
options.open = 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 === DEFAULT_PORT ? (options.port || argv.port) : (argv.port || options.port);
if(options.port) {
startDevServer(wpOpt, options);
return;
}
portfinder.basePort = DEFAULT_PORT;
portfinder.getPort(function(err, port) {
if(err) throw err;
options.port = port;
startDevServer(wpOpt, options);
});
}
function startDevServer(wpOpt, options) {
addDevServerEntrypoints(wpOpt, options);
let compiler;
try {
compiler = webpack(wpOpt);
} catch(e) {
if(e instanceof webpack.WebpackOptionsValidationError) {
console.error(colorError(options.stats.colors, e.message));
process.exit(1); // eslint-disable-line
}
throw e;
}
if(argv["progress"]) {
compiler.apply(new webpack.ProgressPlugin({
profile: argv["profile"]
}));
}
const uri = createDomain(options) + (options.inline !== false || options.lazy === true ? "/" : "/webpack-dev-server/");
let server;
try {
server = new Server(compiler, options);
} catch(e) {
const OptionsValidationError = require("../lib/OptionsValidationError");
if(e instanceof OptionsValidationError) {
console.error(colorError(options.stats.colors, e.message));
process.exit(1); // eslint-disable-line
}
throw e;
}
["SIGINT", "SIGTERM"].forEach(function(sig) {
process.on(sig, function() {
server.close();
process.exit(); // eslint-disable-line no-process-exit
});
});
if(options.socket) {
server.listeningApp.on("error", function(e) {
if(e.code === "EADDRINUSE") {
const clientSocket = new net.Socket();
clientSocket.on("error", function(e) {
if(e.code === "ECONNREFUSED") {
// No other server listening on this socket so it can be safely removed
fs.unlinkSync(options.socket);
server.listen(options.socket, options.host, function(err) {
if(err) throw err;
});
}
});
clientSocket.connect({ path: options.socket }, function() {
throw new Error("This socket is already used");
});
}
});
server.listen(options.socket, options.host, function(err) {
if(err) throw err;
const READ_WRITE = 438; // chmod 666 (rw rw rw)
fs.chmod(options.socket, READ_WRITE, function(err) {
if(err) throw err;
reportReadiness(uri, options);
});
});
} else {
server.listen(options.port, options.host, function(err) {
if(err) throw err;
reportReadiness(uri, options);
});
}
}
function reportReadiness(uri, options) {
const useColor = argv.color;
let startSentence = `Project is running at ${colorInfo(useColor, uri)}`
if(options.socket) {
startSentence = `Listening to socket at ${colorInfo(useColor, options.socket)}`;
}
console.log((argv["progress"] ? "\n" : "") + startSentence);
console.log(`webpack output is served from ${colorInfo(useColor, options.publicPath)}`);
const contentBase = Array.isArray(options.contentBase) ? options.contentBase.join(", ") : options.contentBase;
if(contentBase)
console.log(`Content not from webpack is served from ${colorInfo(useColor, contentBase)}`);
if(options.historyApiFallback)
console.log(`404s will fallback to ${colorInfo(useColor, options.historyApiFallback.index || "/index.html")}`);
if(options.open) {
open(uri).catch(function() {
console.log("Unable to open browser. If you are running in a headless environment, please do not use the open flag.");
});
}
}
processOptions(wpOpt);