#!/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);