WebpackDevServerUtils.js 13.01 KiB
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';
const address = require('address');
const fs = require('fs');
const path = require('path');
const url = require('url');
const chalk = require('chalk');
const detect = require('detect-port-alt');
const isRoot = require('is-root');
const inquirer = require('inquirer');
const clearConsole = require('./clearConsole');
const formatWebpackMessages = require('./formatWebpackMessages');
const getProcessForPort = require('./getProcessForPort');
const isInteractive = process.stdout.isTTY;
let handleCompile;
// You can safely remove this after ejecting.
// We only use this block for testing of Create React App itself:
const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
if (isSmokeTest) {
  handleCompile = (err, stats) => {
    if (err || stats.hasErrors() || stats.hasWarnings()) {
      process.exit(1);
    } else {
      process.exit(0);
function prepareUrls(protocol, host, port) {
  const formatUrl = hostname =>
    url.format({
      protocol,
      hostname,
      port,
      pathname: '/',
    });
  const prettyPrintUrl = hostname =>
    url.format({
      protocol,
      hostname,
      port: chalk.bold(port),
      pathname: '/',
    });
  const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
  let prettyHost, lanUrlForConfig, lanUrlForTerminal;
  if (isUnspecifiedHost) {
    prettyHost = 'localhost';
    try {
      // This can only return an IPv4 address
      lanUrlForConfig = address.ip();
      if (lanUrlForConfig) {
        // Check if the address is a private ip
        // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
        if (
          /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
            lanUrlForConfig
        ) {
          // Address is private, format it for later use
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig); } else { // Address is not private, so we will discard it lanUrlForConfig = undefined; } } } catch (_e) { // ignored } } else { prettyHost = host; } const localUrlForTerminal = prettyPrintUrl(prettyHost); const localUrlForBrowser = formatUrl(prettyHost); return { lanUrlForConfig, lanUrlForTerminal, localUrlForTerminal, localUrlForBrowser, }; } function printInstructions(appName, urls, useYarn) { console.log(); console.log(`You can now view ${chalk.bold(appName)} in the browser.`); console.log(); if (urls.lanUrlForTerminal) { console.log( ` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}` ); console.log( ` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}` ); } else { console.log(` ${urls.localUrlForTerminal}`); } console.log(); console.log('Note that the development build is not optimized.'); console.log( `To create a production build, use ` + `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.` ); console.log(); } function createCompiler(webpack, config, appName, urls, useYarn) { // "Compiler" is a low-level interface to Webpack. // It lets us listen to some events and provide our own custom messages. let compiler; try { compiler = webpack(config, handleCompile); } catch (err) { console.log(chalk.red('Failed to compile.')); console.log(); console.log(err.message || err); console.log(); process.exit(1); } // "invalid" event fires when you have changed a file, and Webpack is // recompiling a bundle. WebpackDevServer takes care to pause serving the // bundle, so if you refresh, it'll wait instead of serving the old one. // "invalid" is short for "bundle invalidated", it doesn't imply any errors. compiler.plugin('invalid', () => { if (isInteractive) { clearConsole(); } console.log('Compiling...');
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
}); let isFirstCompile = true; // "done" event fires when Webpack has finished recompiling the bundle. // Whether or not you have warnings or errors, you will get this event. compiler.plugin('done', stats => { if (isInteractive) { clearConsole(); } // We have switched off the default Webpack output in WebpackDevServer // options so we are going to "massage" the warnings and errors and present // them in a readable focused way. const messages = formatWebpackMessages(stats.toJson({}, true)); const isSuccessful = !messages.errors.length && !messages.warnings.length; if (isSuccessful) { console.log(chalk.green('Compiled successfully!')); } if (isSuccessful && (isInteractive || isFirstCompile)) { printInstructions(appName, urls, useYarn); } isFirstCompile = false; // If errors exist, only show errors. if (messages.errors.length) { console.log(chalk.red('Failed to compile.\n')); console.log(messages.errors.join('\n\n')); return; } // Show warnings if no errors were found. if (messages.warnings.length) { console.log(chalk.yellow('Compiled with warnings.\n')); console.log(messages.warnings.join('\n\n')); // Teach some ESLint tricks. console.log( '\nSearch for the ' + chalk.underline(chalk.yellow('keywords')) + ' to learn more about each warning.' ); console.log( 'To ignore, add ' + chalk.cyan('// eslint-disable-next-line') + ' to the line before.\n' ); } }); return compiler; } function resolveLoopback(proxy) { const o = url.parse(proxy); o.host = undefined; if (o.hostname !== 'localhost') { return proxy; } // Unfortunately, many languages (unlike node) do not yet support IPv6. // This means even though localhost resolves to ::1, the application // must fall back to IPv4 (on 127.0.0.1). // We can re-enable this in a few years. /*try { o.hostname = address.ipv6() ? '::1' : '127.0.0.1'; } catch (_ignored) { o.hostname = '127.0.0.1'; }*/ try { // Check if we're on a network; if we are, chances are we can resolve
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
// localhost. Otherwise, we can just be safe and assume localhost is // IPv4 for maximum compatibility. if (!address.ip()) { o.hostname = '127.0.0.1'; } } catch (_ignored) { o.hostname = '127.0.0.1'; } return url.format(o); } // We need to provide a custom onError function for httpProxyMiddleware. // It allows us to log custom error messages on the console. function onProxyError(proxy) { return (err, req, res) => { const host = req.headers && req.headers.host; console.log( chalk.red('Proxy error:') + ' Could not proxy request ' + chalk.cyan(req.url) + ' from ' + chalk.cyan(host) + ' to ' + chalk.cyan(proxy) + '.' ); console.log( 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' + chalk.cyan(err.code) + ').' ); console.log(); // And immediately send the proper error response to the client. // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side. if (res.writeHead && !res.headersSent) { res.writeHead(500); } res.end( 'Proxy error: Could not proxy request ' + req.url + ' from ' + host + ' to ' + proxy + ' (' + err.code + ').' ); }; } function prepareProxy(proxy, appPublicFolder) { // `proxy` lets you specify alternate servers for specific requests. // It can either be a string or an object conforming to the Webpack dev server proxy configuration // https://webpack.github.io/docs/webpack-dev-server.html if (!proxy) { return undefined; } if (typeof proxy !== 'object' && typeof proxy !== 'string') { console.log( chalk.red( 'When specified, "proxy" in package.json must be a string or an object.' ) ); console.log( chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".') ); console.log( chalk.red(
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
'Either remove "proxy" from package.json, or make it an object.' ) ); process.exit(1); } // Otherwise, if proxy is specified, we will let it handle any request except for files in the public folder. function mayProxy(pathname) { const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1)); return !fs.existsSync(maybePublicPath); } // Support proxy as a string for those who are using the simple proxy option if (typeof proxy === 'string') { if (!/^http(s)?:\/\//.test(proxy)) { console.log( chalk.red( 'When "proxy" is specified in package.json it must start with either http:// or https://' ) ); process.exit(1); } let target; if (process.platform === 'win32') { target = resolveLoopback(proxy); } else { target = proxy; } return [ { target, logLevel: 'silent', // For single page apps, we generally want to fallback to /index.html. // However we also want to respect `proxy` for API calls. // So if `proxy` is specified as a string, we need to decide which fallback to use. // We use a heuristic: if request `accept`s text/html, we pick /index.html. // Modern browsers include text/html into `accept` header when navigating. // However API calls like `fetch()` won’t generally accept text/html. // If this heuristic doesn’t work well for you, use a custom `proxy` object. context: function(pathname, req) { return ( mayProxy(pathname) && req.headers.accept && req.headers.accept.indexOf('text/html') === -1 ); }, onProxyReq: proxyReq => { // Browers may send Origin headers even with same-origin // requests. To prevent CORS issues, we have to change // the Origin to match the target URL. if (proxyReq.getHeader('origin')) { proxyReq.setHeader('origin', target); } }, onError: onProxyError(target), secure: false, changeOrigin: true, ws: true, xfwd: true, }, ]; } // Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer return Object.keys(proxy).map(function(context) { if (!proxy[context].hasOwnProperty('target')) { console.log( chalk.red( 'When `proxy` in package.json is as an object, each `context` object must have a ' +
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
'`target` property specified as a url string' ) ); process.exit(1); } let target; if (process.platform === 'win32') { target = resolveLoopback(proxy[context].target); } else { target = proxy[context].target; } return Object.assign({}, proxy[context], { context: function(pathname) { return mayProxy(pathname) && pathname.match(context); }, onProxyReq: proxyReq => { // Browers may send Origin headers even with same-origin // requests. To prevent CORS issues, we have to change // the Origin to match the target URL. if (proxyReq.getHeader('origin')) { proxyReq.setHeader('origin', target); } }, target, onError: onProxyError(target), }); }); } function choosePort(host, defaultPort) { return detect(defaultPort, host).then( port => new Promise(resolve => { if (port === defaultPort) { return resolve(port); } const message = process.platform !== 'win32' && defaultPort < 1024 && !isRoot() ? `Admin permissions are required to run a server on a port below 1024.` : `Something is already running on port ${defaultPort}.`; if (isInteractive) { clearConsole(); const existingProcess = getProcessForPort(defaultPort); const question = { type: 'confirm', name: 'shouldChangePort', message: chalk.yellow( message + `${existingProcess ? ` Probably:\n ${existingProcess}` : ''}` ) + '\n\nWould you like to run the app on another port instead?', default: true, }; inquirer.prompt(question).then(answer => { if (answer.shouldChangePort) { resolve(port); } else { resolve(null); } }); } else { console.log(chalk.red(message)); resolve(null); } }), err => { throw new Error( chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) + '\n' + ('Network error message: ' + err.message || err) +
421422423424425426427428429430431432433
'\n' ); } ); } module.exports = { choosePort, createCompiler, prepareProxy, prepareUrls, };