diff --git a/lib/Server.js b/lib/Server.js index 92f3db14a8..ac335641bf 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -75,6 +75,12 @@ class Server { this.compiler = compiler; this.options = options; + // Setup default value + this.options.contentBase = + this.options.contentBase !== undefined + ? this.options.contentBase + : process.cwd(); + this.log = _log || createLogger(options); this.originalStats = @@ -218,251 +224,299 @@ class Server { ); } - setupFeatures() { - const app = this.app; - const contentBase = - this.options.contentBase !== undefined - ? this.options.contentBase - : process.cwd(); + 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 proxyConfig; + let proxyMiddleware; + + if (typeof proxyConfigOrCallback === 'function') { + proxyConfig = proxyConfigOrCallback(); + } else { + proxyConfig = 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 (/^(https?:)?\/\//.test(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"*": ""\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:"\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 ( + !/^(https?:)?\/\//.test(contentBase) && + typeof contentBase !== 'number' + ) { + this.app.get('*', serveIndex(contentBase)); + } + } + + setupWatchStaticFeature() { + const contentBase = this.options.contentBase; + + if ( + /^(https?:)?\/\//.test(contentBase) || + typeof contentBase === 'number' + ) { + throw new Error('Watching remote files is not supported.'); + } else if (Array.isArray(contentBase)) { + contentBase.forEach((item) => { + this._watch(item); + }); + } else { + this._watch(contentBase); + } + } + + setupBeforeFeature() { + this.options.before(this.app, this, this.compiler); + } + + setupMiddleware() { + this.app.use(this.middleware); + } + + setupAfterFeature() { + 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) { - // Enable gzip compression. - app.use(compress()); + this.setupCompressFeature(); } }, proxy: () => { if (this.options.proxy) { - /** - * 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 proxyConfig; - let proxyMiddleware; - - if (typeof proxyConfigOrCallback === 'function') { - proxyConfig = proxyConfigOrCallback(); - } else { - proxyConfig = proxyConfigOrCallback; - } - - proxyMiddleware = getProxyMiddleware(proxyConfig); - - if (proxyConfig.ws) { - this.websocketProxies.push(proxyMiddleware); - } - - 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(); - } - }); - }); + this.setupProxyFeature(); } }, historyApiFallback: () => { if (this.options.historyApiFallback) { - const fallback = - typeof this.options.historyApiFallback === 'object' - ? this.options.historyApiFallback - : null; - - // Fall back to /index.html if nothing else matches. - app.use(historyApiFallback(fallback)); + this.setupHistoryApiFallbackFeature(); } }, + // Todo rename to `static` in future major release contentBaseFiles: () => { - if (Array.isArray(contentBase)) { - contentBase.forEach((item) => { - app.get('*', express.static(item)); - }); - } else if (/^(https?:)?\/\//.test(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"*": ""\n}' - ); - // Redirect every request to contentBase - 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:"\n}' - ); - // Redirect every request to the port contentBase - app.get('*', (req, res) => { - res.writeHead(302, { - Location: `//localhost:${contentBase}${req.path}${req._parsedUrl - .search || ''}`, - }); - - res.end(); - }); - } else { - // route content request - app.get('*', express.static(contentBase, this.options.staticOptions)); - } + this.setupStaticFeature(); }, + // Todo rename to `serveIndex` in future major release contentBaseIndex: () => { - if (Array.isArray(contentBase)) { - contentBase.forEach((item) => { - app.get('*', serveIndex(item)); - }); - } else if ( - !/^(https?:)?\/\//.test(contentBase) && - typeof contentBase !== 'number' - ) { - app.get('*', serveIndex(contentBase)); - } + this.setupServeIndexFeature(); }, + // Todo rename to `watchStatic` in future major release watchContentBase: () => { - if ( - /^(https?:)?\/\//.test(contentBase) || - typeof contentBase === 'number' - ) { - throw new Error('Watching remote files is not supported.'); - } else if (Array.isArray(contentBase)) { - contentBase.forEach((item) => { - this._watch(item); - }); - } else { - this._watch(contentBase); - } + this.setupWatchStaticFeature(); }, before: () => { if (typeof this.options.before === 'function') { - this.options.before(app, this, this.compiler); + this.setupBeforeFeature(); } }, middleware: () => { // include our middleware to ensure // it is able to handle '/index.html' request after redirect - app.use(this.middleware); + this.setupMiddleware(); }, after: () => { if (typeof this.options.after === 'function') { - this.options.after(app, this, this.compiler); + this.setupAfterFeature(); } }, headers: () => { - app.all('*', this.setContentHeaders.bind(this)); + this.setupHeadersFeature(); }, magicHtml: () => { - app.get('*', this.serveMagicHtml.bind(this)); + this.setupMagicHtmlFeature(); }, setup: () => { if (typeof this.options.setup === 'function') { - this.log.warn( - 'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`' - ); - - this.options.setup(app, this); + this.setupSetupFeature(); } }, }; @@ -473,6 +527,8 @@ class Server { defaultFeatures.push('proxy', 'middleware'); } + const contentBase = this.options.contentBase; + if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }