From 71a27c0f8be583905b33f757c14324b83d04e51c Mon Sep 17 00:00:00 2001
From: James Blight <james.av.blight@gmail.com>
Date: Mon, 15 May 2017 10:42:54 +0930
Subject: [PATCH] Allow custom proxies in development (#1790)

* Change proxy handling to allow multiple proxies to be specified in package.json.

* Up webpack-dev-server to 2.4.2

Webpack Dev Server version 2.4.2 handles the external websocket upgrade
for all proxies

* Fix the listen() call

* Switch to correct default host

* Remove promises and extract to react-dev-utils

* oops
---
 packages/react-dev-utils/package.json         |   2 +
 packages/react-dev-utils/prepareProxy.js      | 185 +++++++++++++++++
 .../config/webpackDevServer.config.js         | 110 +++++-----
 packages/react-scripts/scripts/start.js       |  50 ++---
 .../scripts/utils/addWebpackMiddleware.js     | 188 ------------------
 5 files changed, 274 insertions(+), 261 deletions(-)
 create mode 100644 packages/react-dev-utils/prepareProxy.js
 delete mode 100644 packages/react-scripts/scripts/utils/addWebpackMiddleware.js

diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json
index 64a368c1b..a5d4c75fe 100644
--- a/packages/react-dev-utils/package.json
+++ b/packages/react-dev-utils/package.json
@@ -23,10 +23,12 @@
     "launchEditor.js",
     "openBrowser.js",
     "openChrome.applescript",
+    "prepareProxy.js",
     "WatchMissingNodeModulesPlugin.js",
     "webpackHotDevClient.js"
   ],
   "dependencies": {
+    "address": "1.0.1",
     "anser": "1.1.0",
     "babel-code-frame": "6.20.0",
     "chalk": "1.1.3",
diff --git a/packages/react-dev-utils/prepareProxy.js b/packages/react-dev-utils/prepareProxy.js
new file mode 100644
index 000000000..ea0e9f163
--- /dev/null
+++ b/packages/react-dev-utils/prepareProxy.js
@@ -0,0 +1,185 @@
+// @remove-on-eject-begin
+/**
+ * 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.
+ */
+// @remove-on-eject-end
+'use strict';
+
+const address = require('address');
+const chalk = require('chalk');
+const url = require('url');
+
+function resolveLoopback(proxy) {
+  const o = url.parse(proxy);
+  o.host = undefined;
+  if (o.hostname !== 'localhost') {
+    return proxy;
+  }
+  try {
+    o.hostname = address.ipv6() ? '::1' : '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 +
+        ').'
+    );
+  };
+}
+
+module.exports = function prepareProxy(proxy) {
+  // `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(
+        '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.
+  // There are a few exceptions which we won't send to the proxy:
+  // - /index.html (served as HTML5 history API fallback)
+  // - /*.hot-update.json (WebpackDevServer uses this too for hot reloading)
+  // - /sockjs-node/* (WebpackDevServer uses this for hot reloading)
+  // Tip: use https://jex.im/regulex/ to visualize the regex
+  const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/;
+
+  // 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.test(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 ' +
+            '`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.test(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),
+    });
+  });
+};
diff --git a/packages/react-scripts/config/webpackDevServer.config.js b/packages/react-scripts/config/webpackDevServer.config.js
index 37d9d1a75..d8c1b885c 100644
--- a/packages/react-scripts/config/webpackDevServer.config.js
+++ b/packages/react-scripts/config/webpackDevServer.config.js
@@ -10,54 +10,74 @@
 // @remove-on-eject-end
 'use strict';
 
+const launchEditor = require('react-dev-utils/launchEditor');
 const config = require('./webpack.config.dev');
 const paths = require('./paths');
 
 const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
-const host = process.env.HOST || 'localhost';
+const host = process.env.HOST || '0.0.0.0';
 
-module.exports = {
-  // Enable gzip compression of generated files.
-  compress: true,
-  // Silence WebpackDevServer's own logs since they're generally not useful.
-  // It will still show compile warnings and errors with this setting.
-  clientLogLevel: 'none',
-  // By default WebpackDevServer serves physical files from current directory
-  // in addition to all the virtual build products that it serves from memory.
-  // This is confusing because those files won’t automatically be available in
-  // production build folder unless we copy them. However, copying the whole
-  // project directory is dangerous because we may expose sensitive files.
-  // Instead, we establish a convention that only files in `public` directory
-  // get served. Our build script will copy `public` into the `build` folder.
-  // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
-  // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
-  // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
-  // Note that we only recommend to use `public` folder as an escape hatch
-  // for files like `favicon.ico`, `manifest.json`, and libraries that are
-  // for some reason broken when imported through Webpack. If you just want to
-  // use an image, put it in `src` and `import` it from JavaScript instead.
-  contentBase: paths.appPublic,
-  // By default files from `contentBase` will not trigger a page reload.
-  watchContentBase: true,
-  // Enable hot reloading server. It will provide /sockjs-node/ endpoint
-  // for the WebpackDevServer client so it can learn when the files were
-  // updated. The WebpackDevServer client is included as an entry point
-  // in the Webpack development configuration. Note that only changes
-  // to CSS are currently hot reloaded. JS changes will refresh the browser.
-  hot: true,
-  // It is important to tell WebpackDevServer to use the same "root" path
-  // as we specified in the config. In development, we always serve from /.
-  publicPath: config.output.publicPath,
-  // WebpackDevServer is noisy by default so we emit custom message instead
-  // by listening to the compiler events with `compiler.plugin` calls above.
-  quiet: true,
-  // Reportedly, this avoids CPU overload on some systems.
-  // https://github.com/facebookincubator/create-react-app/issues/293
-  watchOptions: {
-    ignored: /node_modules/,
-  },
-  // Enable HTTPS if the HTTPS environment variable is set to 'true'
-  https: protocol === 'https',
-  host: host,
-  overlay: false,
+module.exports = function(proxy) {
+  return {
+    // Enable gzip compression of generated files.
+    compress: true,
+    // Silence WebpackDevServer's own logs since they're generally not useful.
+    // It will still show compile warnings and errors with this setting.
+    clientLogLevel: 'none',
+    // By default WebpackDevServer serves physical files from current directory
+    // in addition to all the virtual build products that it serves from memory.
+    // This is confusing because those files won’t automatically be available in
+    // production build folder unless we copy them. However, copying the whole
+    // project directory is dangerous because we may expose sensitive files.
+    // Instead, we establish a convention that only files in `public` directory
+    // get served. Our build script will copy `public` into the `build` folder.
+    // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
+    // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+    // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
+    // Note that we only recommend to use `public` folder as an escape hatch
+    // for files like `favicon.ico`, `manifest.json`, and libraries that are
+    // for some reason broken when imported through Webpack. If you just want to
+    // use an image, put it in `src` and `import` it from JavaScript instead.
+    contentBase: paths.appPublic,
+    // By default files from `contentBase` will not trigger a page reload.
+    watchContentBase: true,
+    // Enable hot reloading server. It will provide /sockjs-node/ endpoint
+    // for the WebpackDevServer client so it can learn when the files were
+    // updated. The WebpackDevServer client is included as an entry point
+    // in the Webpack development configuration. Note that only changes
+    // to CSS are currently hot reloaded. JS changes will refresh the browser.
+    hot: true,
+    // It is important to tell WebpackDevServer to use the same "root" path
+    // as we specified in the config. In development, we always serve from /.
+    publicPath: config.output.publicPath,
+    // WebpackDevServer is noisy by default so we emit custom message instead
+    // by listening to the compiler events with `compiler.plugin` calls above.
+    quiet: true,
+    // Reportedly, this avoids CPU overload on some systems.
+    // https://github.com/facebookincubator/create-react-app/issues/293
+    watchOptions: {
+      ignored: /node_modules/,
+    },
+    // Enable HTTPS if the HTTPS environment variable is set to 'true'
+    https: protocol === 'https',
+    host: host,
+    overlay: false,
+    historyApiFallback: {
+      // Paths with dots should still use the history fallback.
+      // See https://github.com/facebookincubator/create-react-app/issues/387.
+      disableDotRule: true,
+    },
+    proxy,
+    setup(app) {
+      // This lets us open files from the crash overlay.
+      app.use(function launchEditorMiddleware(req, res, next) {
+        if (req.url.startsWith('/__open-stack-frame-in-editor')) {
+          launchEditor(req.query.fileName, req.query.lineNumber);
+          res.end();
+        } else {
+          next();
+        }
+      });
+    },
+  };
 };
diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js
index 4448a0407..69bdc4b74 100644
--- a/packages/react-scripts/scripts/start.js
+++ b/packages/react-scripts/scripts/start.js
@@ -35,7 +35,7 @@ const paths = require('../config/paths');
 const config = require('../config/webpack.config.dev');
 const devServerConfig = require('../config/webpackDevServer.config');
 const createWebpackCompiler = require('./utils/createWebpackCompiler');
-const addWebpackMiddleware = require('./utils/addWebpackMiddleware');
+const prepareProxy = require('react-dev-utils/prepareProxy');
 
 const useYarn = fs.existsSync(paths.yarnLockFile);
 const cli = useYarn ? 'yarn' : 'npm';
@@ -73,34 +73,28 @@ function run(port) {
     }
   );
 
+  // Load proxy config
+  const proxy = require(paths.appPackageJson).proxy;
   // Serve webpack assets generated by the compiler over a web sever.
-  const devServer = new WebpackDevServer(compiler, devServerConfig);
-
-  // Our custom middleware proxies requests to /index.html or a remote API.
-  addWebpackMiddleware(devServer)
-    .then(() => {
-      // Launch WebpackDevServer.
-      devServer.listen(port, HOST, err => {
-        if (err) {
-          return console.log(err);
-        }
-
-        if (isInteractive) {
-          clearConsole();
-        }
-        console.log(chalk.cyan('Starting the development server...'));
-        console.log();
-
-        openBrowser(`${protocol}://${HOST}:${port}/`);
-      });
-    })
-    .catch(e => {
-      console.log(
-        chalk.red('Failed to setup middleware, please report this error:')
-      );
-      console.log(e);
-      process.exit(1);
-    });
+  const devServer = new WebpackDevServer(
+    compiler,
+    devServerConfig(prepareProxy(proxy))
+  );
+
+  // Launch WebpackDevServer.
+  devServer.listen(port, HOST, err => {
+    if (err) {
+      return console.log(err);
+    }
+
+    if (isInteractive) {
+      clearConsole();
+    }
+    console.log(chalk.cyan('Starting the development server...'));
+    console.log();
+
+    openBrowser(`${protocol}://${HOST}:${port}/`);
+  });
 }
 
 // We attempt to use the default port but if it is busy, we offer the user to
diff --git a/packages/react-scripts/scripts/utils/addWebpackMiddleware.js b/packages/react-scripts/scripts/utils/addWebpackMiddleware.js
deleted file mode 100644
index aec088b80..000000000
--- a/packages/react-scripts/scripts/utils/addWebpackMiddleware.js
+++ /dev/null
@@ -1,188 +0,0 @@
-// @remove-on-eject-begin
-/**
- * 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.
- */
-// @remove-on-eject-end
-'use strict';
-
-const chalk = require('chalk');
-const dns = require('dns');
-const historyApiFallback = require('connect-history-api-fallback');
-const httpProxyMiddleware = require('http-proxy-middleware');
-const launchEditor = require('react-dev-utils/launchEditor');
-const url = require('url');
-const paths = require('../../config/paths');
-
-// 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 resolveProxy(proxy) {
-  const p = url.parse(proxy);
-  const hostname = p.hostname;
-  if (hostname !== 'localhost') {
-    return Promise.resolve(proxy);
-  }
-  p.host = undefined; // Remove the host; we don't care about it
-  return new Promise(resolve => {
-    dns.lookup(hostname, { hints: 0, all: false }, (err, address) => {
-      if (err) {
-        console.log(
-          chalk.red(
-            '"proxy" in package.json is set to localhost and cannot be resolved.'
-          )
-        );
-        console.log(
-          chalk.red('Try setting "proxy" to 127.0.0.1 instead of localhost.')
-        );
-        process.exit(1);
-      }
-      p.hostname = address;
-      resolve(url.format(p));
-    });
-  });
-}
-
-function registerProxy(devServer, _proxy) {
-  if (typeof _proxy !== 'string') {
-    console.log(
-      chalk.red('When specified, "proxy" in package.json must be a string.')
-    );
-    console.log(
-      chalk.red('Instead, the type of "proxy" was "' + typeof _proxy + '".')
-    );
-    console.log(
-      chalk.red('Either remove "proxy" from package.json, or make it a string.')
-    );
-    process.exit(1);
-    // Test that proxy url specified starts with http:// or https://
-  } else 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);
-  }
-
-  return (process.platform === 'win32'
-    ? resolveProxy(_proxy)
-    : Promise.resolve(_proxy)).then(proxy => {
-    // Otherwise, if proxy is specified, we will let it handle any request.
-    // There are a few exceptions which we won't send to the proxy:
-    // - /index.html (served as HTML5 history API fallback)
-    // - /*.hot-update.json (WebpackDevServer uses this too for hot reloading)
-    // - /sockjs-node/* (WebpackDevServer uses this for hot reloading)
-    // Tip: use https://jex.im/regulex/ to visualize the regex
-    const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/;
-
-    // Pass the scope regex both to Express and to the middleware for proxying
-    // of both HTTP and WebSockets to work without false positives.
-    const hpm = httpProxyMiddleware(pathname => mayProxy.test(pathname), {
-      target: proxy,
-      logLevel: 'silent',
-      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', proxy);
-        }
-      },
-      onError: onProxyError(proxy),
-      secure: false,
-      changeOrigin: true,
-      ws: true,
-      xfwd: true,
-    });
-    devServer.use(mayProxy, hpm);
-
-    // Listen for the websocket 'upgrade' event and upgrade the connection.
-    // If this is not done, httpProxyMiddleware will not try to upgrade until
-    // an initial plain HTTP request is made.
-    devServer.listeningApp.on('upgrade', hpm.upgrade);
-  });
-}
-
-// This is used by the crash overlay.
-function launchEditorMiddleware() {
-  return function(req, res, next) {
-    if (req.url.startsWith('/__open-stack-frame-in-editor')) {
-      launchEditor(req.query.fileName, req.query.lineNumber);
-      res.end();
-    } else {
-      next();
-    }
-  };
-}
-
-module.exports = function addWebpackMiddleware(devServer) {
-  // `proxy` lets you to specify a fallback server during development.
-  // Every unrecognized request will be forwarded to it.
-  const proxy = require(paths.appPackageJson).proxy;
-  devServer.use(launchEditorMiddleware());
-  devServer.use(
-    historyApiFallback({
-      // Paths with dots should still use the history fallback.
-      // See https://github.com/facebookincubator/create-react-app/issues/387.
-      disableDotRule: true,
-      // 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, 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, don’t use `proxy`.
-      htmlAcceptHeaders: proxy ? ['text/html'] : ['text/html', '*/*'],
-    })
-  );
-  return (proxy
-    ? registerProxy(devServer, proxy)
-    : Promise.resolve()).then(() => {
-    // Finally, by now we have certainly resolved the URL.
-    // It may be /index.html, so let the dev server try serving it again.
-    devServer.use(devServer.middleware);
-  });
-};
-- 
GitLab