UNPKG

create-lbgcli

Version:

前端脚手架模板

1,689 lines (1,418 loc) 67.4 kB
"use strict"; const os = require("os"); const path = require("path"); const url = require("url"); const util = require("util"); const fs = require("graceful-fs"); const ipaddr = require("ipaddr.js"); const internalIp = require("internal-ip"); const express = require("express"); const { validate } = require("schema-utils"); const schema = require("./options.json"); if (!process.env.WEBPACK_SERVE) { process.env.WEBPACK_SERVE = true; } class Server { constructor(options = {}, compiler) { // TODO: remove this after plugin support is published if (options.hooks) { util.deprecate( () => {}, "Using 'compiler' as the first argument is deprecated. Please use 'options' as the first argument and 'compiler' as the second argument.", "DEP_WEBPACK_DEV_SERVER_CONSTRUCTOR" )(); [options = {}, compiler] = [compiler, options]; } validate(schema, options, "webpack Dev Server"); this.options = options; this.staticWatchers = []; // Keep track of websocket proxies for external websocket upgrade. this.webSocketProxies = []; this.sockets = []; this.compiler = compiler; this.currentHash = null; } static get DEFAULT_STATS() { return { all: false, hash: true, warnings: true, errors: true, errorDetails: false, }; } // eslint-disable-next-line class-methods-use-this static isAbsoluteURL(URL) { // Don't match Windows paths `c:\` if (/^[a-zA-Z]:\\/.test(URL)) { return false; } // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL); } static async getHostname(hostname) { if (hostname === "local-ip") { return (await internalIp.v4()) || (await internalIp.v6()) || "0.0.0.0"; } else if (hostname === "local-ipv4") { return (await internalIp.v4()) || "0.0.0.0"; } else if (hostname === "local-ipv6") { return (await internalIp.v6()) || "::"; } return hostname; } static async getFreePort(port) { if (typeof port !== "undefined" && port !== null && port !== "auto") { return port; } const pRetry = require("p-retry"); const portfinder = require("portfinder"); portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 8080; // Try to find unused port and listen on it for 3 times, // if port is not specified in options. const defaultPortRetry = parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) || 3; return pRetry(() => portfinder.getPortPromise(), { retries: defaultPortRetry, }); } static findCacheDir() { const cwd = process.cwd(); let dir = cwd; for (;;) { try { if (fs.statSync(path.join(dir, "package.json")).isFile()) break; // eslint-disable-next-line no-empty } catch (e) {} const parent = path.dirname(dir); if (dir === parent) { // eslint-disable-next-line no-undefined dir = undefined; break; } dir = parent; } if (!dir) { return path.resolve(cwd, ".cache/webpack-dev-server"); } else if (process.versions.pnp === "1") { return path.resolve(dir, ".pnp/.cache/webpack-dev-server"); } else if (process.versions.pnp === "3") { return path.resolve(dir, ".yarn/.cache/webpack-dev-server"); } return path.resolve(dir, "node_modules/.cache/webpack-dev-server"); } addAdditionalEntries(compiler) { const additionalEntries = []; const isWebTarget = compiler.options.externalsPresets ? compiler.options.externalsPresets.web : [ "web", "webworker", "electron-preload", "electron-renderer", "node-webkit", // eslint-disable-next-line no-undefined undefined, null, ].includes(compiler.options.target); // TODO maybe empty empty client if (this.options.client && isWebTarget) { let webSocketURL = ""; if (this.options.webSocketServer) { const searchParams = new URLSearchParams(); /** @type {"ws:" | "wss:" | "http:" | "https:" | "auto:"} */ let protocol; // We are proxying dev server and need to specify custom `hostname` if (typeof this.options.client.webSocketURL.protocol !== "undefined") { protocol = this.options.client.webSocketURL.protocol; } else { protocol = this.options.https ? "wss:" : "ws:"; } searchParams.set("protocol", protocol); if (typeof this.options.client.webSocketURL.username !== "undefined") { searchParams.set( "username", this.options.client.webSocketURL.username ); } if (typeof this.options.client.webSocketURL.password !== "undefined") { searchParams.set( "password", this.options.client.webSocketURL.password ); } /** @type {string} */ let hostname; // SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them // TODO show warning about this const isSockJSType = this.options.webSocketServer.type === "sockjs"; // We are proxying dev server and need to specify custom `hostname` if (typeof this.options.client.webSocketURL.hostname !== "undefined") { hostname = this.options.client.webSocketURL.hostname; } // Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname` else if ( typeof this.options.webSocketServer.options.host !== "undefined" && !isSockJSType ) { hostname = this.options.webSocketServer.options.host; } // The `host` option is specified else if (typeof this.options.host !== "undefined") { hostname = this.options.host; } // The `port` option is not specified else { hostname = "0.0.0.0"; } searchParams.set("hostname", hostname); /** @type {number | string} */ let port; // We are proxying dev server and need to specify custom `port` if (typeof this.options.client.webSocketURL.port !== "undefined") { port = this.options.client.webSocketURL.port; } // Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port` else if ( typeof this.options.webSocketServer.options.port !== "undefined" && !isSockJSType ) { port = this.options.webSocketServer.options.port; } // The `port` option is specified else if (typeof this.options.port === "number") { port = this.options.port; } // The `port` option is specified using `string` else if ( typeof this.options.port === "string" && this.options.port !== "auto" ) { port = Number(this.options.port); } // The `port` option is not specified or set to `auto` else { port = "0"; } searchParams.set("port", String(port)); /** @type {string} */ let pathname = ""; // We are proxying dev server and need to specify custom `pathname` if (typeof this.options.client.webSocketURL.pathname !== "undefined") { pathname = this.options.client.webSocketURL.pathname; } // Web socket server works on custom `path` else if ( typeof this.options.webSocketServer.options.prefix !== "undefined" || typeof this.options.webSocketServer.options.path !== "undefined" ) { pathname = this.options.webSocketServer.options.prefix || this.options.webSocketServer.options.path; } searchParams.set("pathname", pathname); if (typeof this.options.client.logging !== "undefined") { searchParams.set("logging", this.options.client.logging); } webSocketURL = searchParams.toString(); } additionalEntries.push( `${require.resolve("../client/index.js")}?${webSocketURL}` ); } if (this.options.hot) { let hotEntry; if (this.options.hot === "only") { hotEntry = require.resolve("webpack/hot/only-dev-server"); } else if (this.options.hot) { hotEntry = require.resolve("webpack/hot/dev-server"); } additionalEntries.push(hotEntry); } const webpack = compiler.webpack || require("webpack"); // use a hook to add entries if available if (typeof webpack.EntryPlugin !== "undefined") { for (const additionalEntry of additionalEntries) { new webpack.EntryPlugin(compiler.context, additionalEntry, { // eslint-disable-next-line no-undefined name: undefined, }).apply(compiler); } } // TODO remove after drop webpack v4 support else { /** * prependEntry Method for webpack 4 * @param {Entry} originalEntry * @param {Entry} newAdditionalEntries * @returns {Entry} */ const prependEntry = (originalEntry, newAdditionalEntries) => { if (typeof originalEntry === "function") { return () => Promise.resolve(originalEntry()).then((entry) => prependEntry(entry, newAdditionalEntries) ); } 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 const entryDescription = originalEntry[key]; clone[key] = prependEntry(entryDescription, newAdditionalEntries); }); 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; }; compiler.options.entry = prependEntry( compiler.options.entry || "./src", additionalEntries ); compiler.hooks.entryOption.call( compiler.options.context, compiler.options.entry ); } } getCompilerOptions() { if (typeof this.compiler.compilers !== "undefined") { if (this.compiler.compilers.length === 1) { return this.compiler.compilers[0].options; } // Configuration with the `devServer` options const compilerWithDevServer = this.compiler.compilers.find( (config) => config.options.devServer ); if (compilerWithDevServer) { return compilerWithDevServer.options; } // Configuration with `web` preset const compilerWithWebPreset = this.compiler.compilers.find( (config) => (config.options.externalsPresets && config.options.externalsPresets.web) || [ "web", "webworker", "electron-preload", "electron-renderer", "node-webkit", // eslint-disable-next-line no-undefined undefined, null, ].includes(config.options.target) ); if (compilerWithWebPreset) { return compilerWithWebPreset.options; } // Fallback return this.compiler.compilers[0].options; } return this.compiler.options; } // eslint-disable-next-line class-methods-use-this async normalizeOptions() { const { options } = this; if (!this.logger) { this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server"); } const compilerOptions = this.getCompilerOptions(); // TODO remove `{}` after drop webpack v4 support const watchOptions = compilerOptions.watchOptions || {}; const defaultOptionsForStatic = { directory: path.join(process.cwd(), "public"), staticOptions: {}, publicPath: ["/"], serveIndex: { icons: true }, // Respect options from compiler watchOptions watch: watchOptions, }; if (typeof options.allowedHosts === "undefined") { // AllowedHosts allows some default hosts picked from `options.host` or `webSocketURL.hostname` and `localhost` options.allowedHosts = "auto"; } // We store allowedHosts as array when supplied as string else if ( typeof options.allowedHosts === "string" && options.allowedHosts !== "auto" && options.allowedHosts !== "all" ) { options.allowedHosts = [options.allowedHosts]; } // CLI pass options as array, we should normalize them else if ( Array.isArray(options.allowedHosts) && options.allowedHosts.includes("all") ) { options.allowedHosts = "all"; } if (typeof options.bonjour === "undefined") { options.bonjour = false; } else if (typeof options.bonjour === "boolean") { options.bonjour = options.bonjour ? {} : false; } if ( typeof options.client === "undefined" || (typeof options.client === "object" && options.client !== null) ) { if (!options.client) { options.client = {}; } if (typeof options.client.webSocketURL === "undefined") { options.client.webSocketURL = {}; } else if (typeof options.client.webSocketURL === "string") { const parsedURL = new URL(options.client.webSocketURL); options.client.webSocketURL = { protocol: parsedURL.protocol, hostname: parsedURL.hostname, port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "", pathname: parsedURL.pathname, username: parsedURL.username, password: parsedURL.password, }; } else if (typeof options.client.webSocketURL.port === "string") { options.client.webSocketURL.port = Number( options.client.webSocketURL.port ); } // Enable client overlay by default if (typeof options.client.overlay === "undefined") { options.client.overlay = true; } else if (typeof options.client.overlay !== "boolean") { options.client.overlay = { errors: true, warnings: true, ...options.client.overlay, }; } // Respect infrastructureLogging.level if (typeof options.client.logging === "undefined") { options.client.logging = compilerOptions.infrastructureLogging ? compilerOptions.infrastructureLogging.level : "info"; } } if (typeof options.compress === "undefined") { options.compress = true; } if (typeof options.devMiddleware === "undefined") { options.devMiddleware = {}; } // No need to normalize `headers` if (typeof options.historyApiFallback === "undefined") { options.historyApiFallback = false; } else if ( typeof options.historyApiFallback === "boolean" && options.historyApiFallback ) { options.historyApiFallback = {}; } // No need to normalize `host` options.hot = typeof options.hot === "boolean" || options.hot === "only" ? options.hot : true; // if the user enables http2, we can safely enable https if ((options.http2 && !options.https) || options.https === true) { options.https = { requestCert: false, }; } // https option if (options.https) { // TODO remove the `cacert` option in favor `ca` in the next major release for (const property of ["cacert", "ca", "cert", "crl", "key", "pfx"]) { if (typeof options.https[property] === "undefined") { // eslint-disable-next-line no-continue continue; } const value = options.https[property]; const readFile = (item) => { if ( Buffer.isBuffer(item) || (typeof item === "object" && item !== null && !Array.isArray(item)) ) { return item; } if (item) { let stats = null; try { stats = fs.lstatSync(fs.realpathSync(item)).isFile(); } catch (error) { // Ignore error } // It is file return stats ? fs.readFileSync(item) : item; } }; options.https[property] = Array.isArray(value) ? value.map((item) => readFile(item)) : readFile(value); } let fakeCert; if (!options.https.key || !options.https.cert) { const certificateDir = Server.findCacheDir(); const certificatePath = path.join(certificateDir, "server.pem"); let certificateExists; try { const certificate = await fs.promises.stat(certificatePath); certificateExists = certificate.isFile(); } catch { certificateExists = false; } if (certificateExists) { const certificateTtl = 1000 * 60 * 60 * 24; const certificateStat = await fs.promises.stat(certificatePath); const now = new Date(); // cert is more than 30 days old, kill it with fire if ((now - certificateStat.ctime) / certificateTtl > 30) { const del = require("del"); this.logger.info( "SSL Certificate is more than 30 days old. Removing..." ); await del([certificatePath], { force: true }); certificateExists = false; } } if (!certificateExists) { this.logger.info("Generating SSL Certificate..."); const selfsigned = require("selfsigned"); const attributes = [{ name: "commonName", value: "localhost" }]; const pems = 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", }, ], }, ], }); await fs.promises.mkdir(certificateDir, { recursive: true }); await fs.promises.writeFile( certificatePath, pems.private + pems.cert, { encoding: "utf8", } ); } fakeCert = await fs.promises.readFile(certificatePath); this.logger.info(`SSL certificate: ${certificatePath}`); } if (options.https.cacert) { if (options.https.ca) { this.logger.warn( "Do not specify 'https.ca' and 'https.cacert' options together, the 'https.ca' option will be used." ); } else { options.https.ca = options.https.cacert; } delete options.https.cacert; } options.https.key = options.https.key || fakeCert; options.https.cert = options.https.cert || fakeCert; } if (typeof options.ipc === "boolean") { const isWindows = process.platform === "win32"; const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os.tmpdir(); const pipeName = "webpack-dev-server.sock"; options.ipc = path.join(pipePrefix, pipeName); } options.liveReload = typeof options.liveReload !== "undefined" ? options.liveReload : true; options.magicHtml = typeof options.magicHtml !== "undefined" ? options.magicHtml : true; // https://github.com/webpack/webpack-dev-server/issues/1990 const defaultOpenOptions = { wait: false }; const getOpenItemsFromObject = ({ target, ...rest }) => { const normalizedOptions = { ...defaultOpenOptions, ...rest }; if (typeof normalizedOptions.app === "string") { normalizedOptions.app = { name: normalizedOptions.app, }; } const normalizedTarget = typeof target === "undefined" ? "<url>" : target; if (Array.isArray(normalizedTarget)) { return normalizedTarget.map((singleTarget) => { return { target: singleTarget, options: normalizedOptions }; }); } return [{ target: normalizedTarget, options: normalizedOptions }]; }; if (typeof options.open === "undefined") { options.open = []; } else if (typeof options.open === "boolean") { options.open = options.open ? [{ target: "<url>", options: defaultOpenOptions }] : []; } else if (typeof options.open === "string") { options.open = [{ target: options.open, options: defaultOpenOptions }]; } else if (Array.isArray(options.open)) { const result = []; options.open.forEach((item) => { if (typeof item === "string") { result.push({ target: item, options: defaultOpenOptions }); return; } result.push(...getOpenItemsFromObject(item)); }); options.open = result; } else { options.open = [...getOpenItemsFromObject(options.open)]; } if (typeof options.port === "string" && options.port !== "auto") { options.port = Number(options.port); } /** * Assume a proxy configuration specified as: * proxy: { * 'context': { options } * } * OR * proxy: { * 'context': 'target' * } */ if (typeof options.proxy !== "undefined") { // TODO remove in the next major release, only accept `Array` if (!Array.isArray(options.proxy)) { if ( Object.prototype.hasOwnProperty.call(options.proxy, "target") || Object.prototype.hasOwnProperty.call(options.proxy, "router") ) { options.proxy = [options.proxy]; } else { options.proxy = Object.keys(options.proxy).map((context) => { let proxyOptions; // For backwards compatibility reasons. const 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; }); } } options.proxy = options.proxy.map((item) => { const getLogLevelForProxy = (level) => { if (level === "none") { return "silent"; } if (level === "log") { return "info"; } if (level === "verbose") { return "debug"; } return level; }; if (typeof item.logLevel === "undefined") { item.logLevel = getLogLevelForProxy( compilerOptions.infrastructureLogging ? compilerOptions.infrastructureLogging.level : "info" ); } if (typeof item.logProvider === "undefined") { item.logProvider = () => this.logger; } return item; }); } if (typeof options.setupExitSignals === "undefined") { options.setupExitSignals = true; } if (typeof options.static === "undefined") { options.static = [defaultOptionsForStatic]; } else if (typeof options.static === "boolean") { options.static = options.static ? [defaultOptionsForStatic] : false; } else if (typeof options.static === "string") { options.static = [ { ...defaultOptionsForStatic, directory: options.static }, ]; } else if (Array.isArray(options.static)) { options.static = options.static.map((item) => { if (typeof item === "string") { return { ...defaultOptionsForStatic, directory: item }; } return { ...defaultOptionsForStatic, ...item }; }); } else { options.static = [{ ...defaultOptionsForStatic, ...options.static }]; } if (options.static) { options.static.forEach((staticOption) => { if (Server.isAbsoluteURL(staticOption.directory)) { throw new Error("Using a URL as static.directory is not supported"); } // ensure that publicPath is an array if (typeof staticOption.publicPath === "string") { staticOption.publicPath = [staticOption.publicPath]; } // ensure that watch is an object if true if (staticOption.watch === true) { staticOption.watch = defaultOptionsForStatic.watch; } // ensure that serveIndex is an object if true if (staticOption.serveIndex === true) { staticOption.serveIndex = defaultOptionsForStatic.serveIndex; } }); } if (typeof options.watchFiles === "string") { options.watchFiles = [{ paths: options.watchFiles, options: {} }]; } else if ( typeof options.watchFiles === "object" && options.watchFiles !== null && !Array.isArray(options.watchFiles) ) { options.watchFiles = [ { paths: options.watchFiles.paths, options: options.watchFiles.options || {}, }, ]; } else if (Array.isArray(options.watchFiles)) { options.watchFiles = options.watchFiles.map((item) => { if (typeof item === "string") { return { paths: item, options: {} }; } return { paths: item.paths, options: item.options || {} }; }); } else { options.watchFiles = []; } const defaultWebSocketServerType = "ws"; const defaultWebSocketServerOptions = { path: "/ws" }; if (typeof options.webSocketServer === "undefined") { options.webSocketServer = { type: defaultWebSocketServerType, options: defaultWebSocketServerOptions, }; } else if ( typeof options.webSocketServer === "boolean" && !options.webSocketServer ) { options.webSocketServer = false; } else if ( typeof options.webSocketServer === "string" || typeof options.webSocketServer === "function" ) { options.webSocketServer = { type: options.webSocketServer, options: defaultWebSocketServerOptions, }; } else { options.webSocketServer = { type: options.webSocketServer.type || defaultWebSocketServerType, options: { ...defaultWebSocketServerOptions, ...options.webSocketServer.options, }, }; if (typeof options.webSocketServer.options.port === "string") { options.webSocketServer.options.port = Number( options.webSocketServer.options.port ); } } } getClientTransport() { let ClientImplementation; let clientImplementationFound = true; const isKnownWebSocketServerImplementation = this.options.webSocketServer && typeof this.options.webSocketServer.type === "string" && (this.options.webSocketServer.type === "ws" || this.options.webSocketServer.type === "sockjs"); let clientTransport; if (this.options.client) { if (typeof this.options.client.webSocketTransport !== "undefined") { clientTransport = this.options.client.webSocketTransport; } else if (isKnownWebSocketServerImplementation) { clientTransport = this.options.webSocketServer.type; } else { clientTransport = "ws"; } } else { clientTransport = "ws"; } switch (typeof clientTransport) { case "string": // could be 'sockjs', 'ws', or a path that should be required if (clientTransport === "sockjs") { ClientImplementation = require.resolve( "../client/clients/SockJSClient" ); } else if (clientTransport === "ws") { ClientImplementation = require.resolve( "../client/clients/WebSocketClient" ); } else { try { // eslint-disable-next-line import/no-dynamic-require ClientImplementation = require.resolve(clientTransport); } catch (e) { clientImplementationFound = false; } } break; default: clientImplementationFound = false; } if (!clientImplementationFound) { throw new Error( `${ !isKnownWebSocketServerImplementation ? "When you use custom web socket implementation you must explicitly specify client.webSocketTransport. " : "" }client.webSocketTransport must be a string denoting a default implementation (e.g. 'sockjs', 'ws') or a full path to a JS file via require.resolve(...) which exports a class ` ); } return ClientImplementation; } getServerTransport() { let implementation; let implementationFound = true; switch (typeof this.options.webSocketServer.type) { case "string": // Could be 'sockjs', in the future 'ws', or a path that should be required if (this.options.webSocketServer.type === "sockjs") { implementation = require("./servers/SockJSServer"); } else if (this.options.webSocketServer.type === "ws") { implementation = require("./servers/WebsocketServer"); } else { try { // eslint-disable-next-line import/no-dynamic-require implementation = require(this.options.webSocketServer.type); } catch (error) { implementationFound = false; } } break; case "function": implementation = this.options.webSocketServer.type; break; default: implementationFound = false; } if (!implementationFound) { throw new Error( "webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " + "a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " + "via require.resolve(...), or the class itself which extends BaseServer" ); } return implementation; } setupProgressPlugin() { const { ProgressPlugin } = this.compiler.webpack || require("webpack"); new ProgressPlugin((percent, msg, addInfo, pluginName) => { percent = Math.floor(percent * 100); if (percent === 100) { msg = "Compilation completed"; } if (addInfo) { msg = `${msg} (${addInfo})`; } if (this.webSocketServer) { this.sendMessage(this.webSocketServer.clients, "progress-update", { percent, msg, pluginName, }); } if (this.server) { this.server.emit("progress-update", { percent, msg, pluginName }); } }).apply(this.compiler); } async initialize() { if (this.options.webSocketServer) { const compilers = this.compiler.compilers || [this.compiler]; // eslint-disable-next-line no-shadow compilers.forEach((compiler) => { this.addAdditionalEntries(compiler); const webpack = compiler.webpack || require("webpack"); new webpack.ProvidePlugin({ __webpack_dev_server_client__: this.getClientTransport(), }).apply(compiler); // TODO remove after drop webpack v4 support compiler.options.plugins = compiler.options.plugins || []; if (this.options.hot) { const HMRPluginExists = compiler.options.plugins.find( (p) => p.constructor === webpack.HotModuleReplacementPlugin ); if (HMRPluginExists) { this.logger.warn( `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.` ); } else { // Apply the HMR plugin const plugin = new webpack.HotModuleReplacementPlugin(); plugin.apply(compiler); } } }); if (this.options.client && this.options.client.progress) { this.setupProgressPlugin(); } } this.setupHooks(); this.setupApp(); this.setupHostHeaderCheck(); this.setupDevMiddleware(); // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response this.setupBuiltInRoutes(); this.setupWatchFiles(); this.setupFeatures(); this.createServer(); if (this.options.setupExitSignals) { const signals = ["SIGINT", "SIGTERM"]; let needForceShutdown = false; const exitProcess = () => { // eslint-disable-next-line no-process-exit process.exit(); }; signals.forEach((signal) => { process.on(signal, () => { if (needForceShutdown) { exitProcess(); } this.logger.info( "Gracefully shutting down. To force exit, press ^C again. Please wait..." ); needForceShutdown = true; this.stopCallback(() => { if (typeof this.compiler.close === "function") { this.compiler.close(exitProcess); } else { exitProcess(); } }); }); }); } // Proxy WebSocket without the initial http request // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade // eslint-disable-next-line func-names this.webSocketProxies.forEach(function (webSocketProxy) { this.server.on("upgrade", webSocketProxy.upgrade); }, this); } setupApp() { // Init express server // eslint-disable-next-line new-cap this.app = new express(); } getStats(statsObj) { const stats = Server.DEFAULT_STATS; const compilerOptions = this.getCompilerOptions(); if (compilerOptions.stats && compilerOptions.stats.warningsFilter) { stats.warningsFilter = compilerOptions.stats.warningsFilter; } return statsObj.toJson(stats); } setupHooks() { this.compiler.hooks.invalid.tap("webpack-dev-server", () => { if (this.webSocketServer) { this.sendMessage(this.webSocketServer.clients, "invalid"); } }); this.compiler.hooks.done.tap("webpack-dev-server", (stats) => { if (this.webSocketServer) { this.sendStats(this.webSocketServer.clients, this.getStats(stats)); } this.stats = stats; }); } setupHostHeaderCheck() { this.app.all("*", (req, res, next) => { if (this.checkHeader(req.headers, "host")) { return next(); } res.send("Invalid Host header"); }); } setupDevMiddleware() { const webpackDevMiddleware = require("webpack-dev-middleware"); // middleware for serving webpack bundle this.middleware = webpackDevMiddleware( this.compiler, this.options.devMiddleware ); } setupBuiltInRoutes() { const { app, middleware } = this; app.get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => { res.setHeader("Content-Type", "application/javascript"); const { createReadStream } = fs; const clientPath = path.join(__dirname, "..", "client"); createReadStream( path.join(clientPath, "modules/sockjs-client/index.js") ).pipe(res); }); app.get("/webpack-dev-server/invalidate", (_req, res) => { this.invalidate(); res.end(); }); app.get("/webpack-dev-server", (req, res) => { middleware.waitUntilValid((stats) => { res.setHeader("Content-Type", "text/html"); res.write( '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>' ); const statsForPrint = typeof stats.stats !== "undefined" ? stats.toJson().children : [stats.toJson()]; res.write(`<h1>Assets Report:</h1>`); statsForPrint.forEach((item, index) => { res.write("<div>"); const name = item.name || (stats.stats ? `unnamed[${index}]` : "unnamed"); res.write(`<h2>Compilation: ${name}</h2>`); res.write("<ul>"); const publicPath = item.publicPath === "auto" ? "" : item.publicPath; for (const asset of item.assets) { const assetName = asset.name; const assetURL = `${publicPath}${assetName}`; res.write( `<li> <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong> </li>` ); } res.write("</ul>"); res.write("</div>"); }); res.end("</body></html>"); }); }); } setupCompressFeature() { const compress = require("compression"); this.app.use(compress()); } setupProxyFeature() { const { createProxyMiddleware } = require("http-proxy-middleware"); const getProxyMiddleware = (proxyConfig) => { // It is possible to use the `bypass` method without a `target` or `router`. // However, the proxy middleware has no use in this case, and will fail to instantiate. if (proxyConfig.target) { const context = proxyConfig.context || proxyConfig.path; return createProxyMiddleware(context, proxyConfig); } if (proxyConfig.router) { return createProxyMiddleware(proxyConfig); } }; /** * Assume a proxy configuration specified as: * proxy: [ * { * context: "value", * ...options, * }, * // or: * function() { * return { * context: "context", * ...options, * }; * } * ] */ this.options.proxy.forEach((proxyConfigOrCallback) => { let proxyMiddleware; let proxyConfig = typeof proxyConfigOrCallback === "function" ? proxyConfigOrCallback() : proxyConfigOrCallback; proxyMiddleware = getProxyMiddleware(proxyConfig); if (proxyConfig.ws) { this.webSocketProxies.push(proxyMiddleware); } const handle = async (req, res, next) => { if (typeof proxyConfigOrCallback === "function") { const newProxyConfig = proxyConfigOrCallback(req, res, next); 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 // TODO remove in the next major in favor `context` and `router` options const isByPassFuncDefined = typeof proxyConfig.bypass === "function"; const bypassUrl = isByPassFuncDefined ? await proxyConfig.bypass(req, res, proxyConfig) : null; if (typeof bypassUrl === "boolean") { // skip the proxy req.url = null; next(); } else if (typeof bypassUrl === "string") { // byPass to that url req.url = bypassUrl; next(); } else if (proxyMiddleware) { return proxyMiddleware(req, res, next); } else { next(); } }; this.app.use(handle); // Also forward error requests to the proxy so it can handle them. this.app.use((error, req, res, next) => handle(req, res, next)); }); } setupHistoryApiFallbackFeature() { const { historyApiFallback } = this.options; if ( typeof historyApiFallback.logger === "undefined" && !historyApiFallback.verbose ) { historyApiFallback.logger = this.logger.log.bind( this.logger, "[connect-history-api-fallback]" ); } // Fall back to /index.html if nothing else matches. this.app.use(require("connect-history-api-fallback")(historyApiFallback)); } setupStaticFeature() { this.options.static.forEach((staticOption) => { staticOption.publicPath.forEach((publicPath) => { this.app.use( publicPath, express.static(staticOption.directory, staticOption.staticOptions) ); }); }); } setupStaticServeIndexFeature() { const serveIndex = require("serve-index"); this.options.static.forEach((staticOption) => { staticOption.publicPath.forEach((publicPath) => { if (staticOption.serveIndex) { this.app.use(publicPath, (req, res, next) => { // serve-index doesn't fallthrough non-get/head request to next middleware if (req.method !== "GET" && req.method !== "HEAD") { return next(); } serveIndex(staticOption.directory, staticOption.serveIndex)( req, res, next ); }); } }); }); } setupStaticWatchFeature() { this.options.static.forEach((staticOption) => { if (staticOption.watch) { this.watchFiles(staticOption.directory, staticOption.watch); } }); } setupOnBeforeSetupMiddlewareFeature() { this.options.onBeforeSetupMiddleware(this); } setupWatchFiles() { const { watchFiles } = this.options; if (watchFiles.length > 0) { watchFiles.forEach((item) => { this.watchFiles(item.paths, item.options); }); } } setupMiddleware() { this.app.use(this.middleware); } setupOnAfterSetupMiddlewareFeature() { this.options.onAfterSetupMiddleware(this); } setupHeadersFeature() { this.app.all("*", this.setHeaders.bind(this)); } setupMagicHtmlFeature() { this.app.get("*", this.serveMagicHtml.bind(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(); } }, static: () => { this.setupStaticFeature(); }, staticServeIndex: () => { this.setupStaticServeIndexFeature(); }, staticWatch: () => { this.setupStaticWatchFeature(); }, onBeforeSetupMiddleware: () => { if (typeof this.options.onBeforeSetupMiddleware === "function") { this.setupOnBeforeSetupMiddlewareFeature(); } }, onAfterSetupMiddleware: () => { if (typeof this.options.onAfterSetupMiddleware === "function") { this.setupOnAfterSetupMiddlewareFeature(); } }, middleware: () => { // include our middleware to ensure // it is able to handle '/index.html' request after redirect this.setupMiddleware(); }, headers: () => { this.setupHeadersFeature(); }, magicHtml: () => { this.setupMagicHtmlFeature(); }, }; 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"); } if (this.options.onBeforeSetupMiddleware) { runnableFeatures.push("onBeforeSetupMiddleware"); } runnableFeatures.push("headers", "middleware"); if (this.options.proxy) { runnableFeatures.push("proxy", "middleware"); } if (this.options.static) { runnableFeatures.push("static"); } if (this.options.historyApiFallback) { runnableFeatures.push("historyApiFallback", "middleware"); if (this.options.static) { runnableFeatures.push("static"); } } if (this.options.static) { runnableFeatures.push("staticServeIndex", "staticWatch"); } if (this.options.magicHtml) { runnableFeatures.push("magicHtml"); } if (this.options.onAfterSetupMiddleware) { runnableFeatures.push("onAfterSetupMiddleware"); } runnableFeatures.forEach((feature) => { features[feature](); }); } createServer() { if (this.options.https) { if (this.options.http2) { // TODO: we need to replace spdy with http2 which is an internal module this.server = require("spdy").createServer( { ...this.options.https, spdy: { protocols: ["h2", "http/1.1"], }, }, this.app ); } else { const https = require("https"); this.server = https.createServer(this.options.https, this.app); } } else { const http = require("http"); this.server = http.createServer(this.app); } this.server.on("connection", (socket) => { // Add socket to list this.sockets.push(socket); socket.once("close", () => { // Remove socket from list this.sockets.splice(this.sockets.indexOf(socket), 1); }); }); this.server.on("error", (error) => { throw error; }); } createWebSocketServer() { this.webSocketServer = new (this.getServerTransport())(this); this.webSocketServer.implementation.on("connection", (client, request) => { const headers = // eslint-disable-next-line no-nested-ternary typeof request !== "undefined" ? request.headers : typeof client.headers !== "undefined" ? client.headers : // eslint-disable-next-line no-undefined undefined; if (!headers) { this.logger.warn( 'webSocketServer implementation must pass headers for the "connection" event' ); } if ( !headers || !this.checkHeader(headers, "host") || !this.checkHeader(headers, "origin") ) { this.sendMessage([client], "error", "Invalid Host/Origin header"); client.terminate(); return; } if (this.options.hot === true || this.options.hot === "only") { this.sendMessage([client], "hot"); } if (this.options.liveReload) { this.sendMessage([client], "liveReload"); } if (this.options.client && this.options.client.progress) { this.sendMessage([client], "progress", this.options.client.progress); } if (this.options.client && this.options.client.overlay) { this.sendMessage([client], "overlay", this.options.client.overlay); } if (!this.stats) { return; } this.sendStats([client], this.getStats(this.stats), true); }); } openBrowser(defaultOpenTarget) { const open = require("open"); Promise.all( this.options.open.map((item) => { let openTarget; if (item.target === "<url>") { openTarget = defaultOpenTarget; } else { openTarget = Server.isAbsoluteURL(item.target) ? item.target : new URL(item.target, defaultOpenTarget).toString(); } return open(openTarget, item.options).catch(() => { this.logger.warn( `Unable to open "${openTarget}" page${ // eslint-disable-next-line no-nested-ternary item.options.app ? ` in "${item.options.app.name}" app${ item.options.app.arguments ? ` with "${item.options.app.arguments.join( " " )}" arguments` : "" }` : "" }. If you are running in a headless environment, please do not use the "open" option or related flags like "--open", "--open-target", and "--open-app".` ); }); }) ); } runBonjour() { const bonjour = require("bonjour")(); bonjour.publish({ name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`, port: this.options.port, type: this.options.https ? "https" : "http", subtypes: ["webpack"], ...this.options.bonjour, }); process.on("exit", () => { bonjour.unpub