From 4434467a438c3d431a5169038a5f481bbf763efe Mon Sep 17 00:00:00 2001
From: Dan Abramov <dan.abramov@gmail.com>
Date: Sun, 14 May 2017 12:17:05 +0100
Subject: [PATCH] Click to view source from error overlay (#2141)

* Click to view source

* Update package.json

* Update package.json

* Fix lint
---
 packages/react-dev-utils/launchEditor.js      | 162 ++++++++++++++++++
 packages/react-dev-utils/package.json         |   2 +
 .../src/components/code.js                    |  17 +-
 .../src/components/frame.js                   |   8 +-
 .../scripts/utils/addWebpackMiddleware.js     |  14 ++
 5 files changed, 200 insertions(+), 3 deletions(-)
 create mode 100644 packages/react-dev-utils/launchEditor.js

diff --git a/packages/react-dev-utils/launchEditor.js b/packages/react-dev-utils/launchEditor.js
new file mode 100644
index 000000000..768e883a8
--- /dev/null
+++ b/packages/react-dev-utils/launchEditor.js
@@ -0,0 +1,162 @@
+/**
+ * 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';
+
+var fs = require('fs');
+var path = require('path');
+var child_process = require('child_process');
+const shellQuote = require('shell-quote');
+
+function isTerminalEditor(editor) {
+  switch (editor) {
+    case 'vim':
+    case 'emacs':
+    case 'nano':
+      return true;
+  }
+  return false;
+}
+
+// Map from full process name to binary that starts the process
+// We can't just re-use full process name, because it will spawn a new instance
+// of the app every time
+var COMMON_EDITORS = {
+  '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
+  '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta': '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
+  '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text': '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
+  '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2': '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
+  '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
+};
+
+function addWorkspaceToArgumentsIfExists(args, workspace) {
+  if (workspace) {
+    args.unshift(workspace);
+  }
+  return args;
+}
+
+function getArgumentsForLineNumber(editor, fileName, lineNumber, workspace) {
+  switch (path.basename(editor)) {
+    case 'vim':
+    case 'mvim':
+      return [fileName, '+' + lineNumber];
+    case 'atom':
+    case 'Atom':
+    case 'Atom Beta':
+    case 'subl':
+    case 'sublime':
+    case 'wstorm':
+    case 'appcode':
+    case 'charm':
+    case 'idea':
+      return [fileName + ':' + lineNumber];
+    case 'joe':
+    case 'emacs':
+    case 'emacsclient':
+      return ['+' + lineNumber, fileName];
+    case 'rmate':
+    case 'mate':
+    case 'mine':
+      return ['--line', lineNumber, fileName];
+    case 'code':
+      return addWorkspaceToArgumentsIfExists(
+        ['-g', fileName + ':' + lineNumber],
+        workspace
+      );
+  }
+
+  // For all others, drop the lineNumber until we have
+  // a mapping above, since providing the lineNumber incorrectly
+  // can result in errors or confusing behavior.
+  return [fileName];
+}
+
+function guessEditor() {
+  // Explicit config always wins
+  if (process.env.REACT_EDITOR) {
+    return shellQuote.parse(process.env.REACT_EDITOR);
+  }
+
+  // Using `ps x` on OSX we can find out which editor is currently running.
+  // Potentially we could use similar technique for Windows and Linux
+  if (process.platform === 'darwin') {
+    try {
+      var output = child_process.execSync('ps x').toString();
+      var processNames = Object.keys(COMMON_EDITORS);
+      for (var i = 0; i < processNames.length; i++) {
+        var processName = processNames[i];
+        if (output.indexOf(processName) !== -1) {
+          return [COMMON_EDITORS[processName]];
+        }
+      }
+    } catch (error) {
+      // Ignore...
+    }
+  }
+
+  // Last resort, use old skool env vars
+  if (process.env.VISUAL) {
+    return [process.env.VISUAL];
+  } else if (process.env.EDITOR) {
+    return [process.env.EDITOR];
+  }
+
+  return [null];
+}
+
+var _childProcess = null;
+function launchEditor(fileName, lineNumber) {
+  if (!fs.existsSync(fileName)) {
+    return;
+  }
+
+  // Sanitize lineNumber to prevent malicious use on win32
+  // via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
+  if (lineNumber && isNaN(lineNumber)) {
+    return;
+  }
+
+  let [editor, ...args] = guessEditor();
+  if (!editor) {
+    return;
+  }
+
+  var workspace = null;
+  if (lineNumber) {
+    args = args.concat(
+      getArgumentsForLineNumber(editor, fileName, lineNumber, workspace)
+    );
+  } else {
+    args.push(fileName);
+  }
+
+  if (_childProcess && isTerminalEditor(editor)) {
+    // There's an existing editor process already and it's attached
+    // to the terminal, so go kill it. Otherwise two separate editor
+    // instances attach to the stdin/stdout which gets confusing.
+    _childProcess.kill('SIGKILL');
+  }
+
+  if (process.platform === 'win32') {
+    // On Windows, launch the editor in a shell because spawn can only
+    // launch .exe files.
+    _childProcess = child_process.spawn(
+      'cmd.exe',
+      ['/C', editor].concat(args),
+      { stdio: 'inherit' }
+    );
+  } else {
+    _childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
+  }
+  _childProcess.on('exit', function() {
+    _childProcess = null;
+  });
+}
+
+module.exports = launchEditor;
diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json
index 8fe7f53b3..9edb99875 100644
--- a/packages/react-dev-utils/package.json
+++ b/packages/react-dev-utils/package.json
@@ -19,6 +19,7 @@
     "formatWebpackMessages.js",
     "getProcessForPort.js",
     "InterpolateHtmlPlugin.js",
+    "launchEditor.js",
     "openBrowser.js",
     "openChrome.applescript",
     "prompt.js",
@@ -35,6 +36,7 @@
     "html-entities": "1.2.0",
     "opn": "4.0.2",
     "recursive-readdir": "2.1.1",
+    "shell-quote": "^1.6.1",
     "sockjs-client": "1.1.2",
     "stack-frame-mapper": "0.4.0",
     "stack-frame-parser": "0.4.0",
diff --git a/packages/react-error-overlay/src/components/code.js b/packages/react-error-overlay/src/components/code.js
index f25ed8378..e27e72fd7 100644
--- a/packages/react-error-overlay/src/components/code.js
+++ b/packages/react-error-overlay/src/components/code.js
@@ -19,7 +19,9 @@ function createCode(
   lineNum: number,
   columnNum: number | null,
   contextSize: number,
-  main: boolean = false
+  main: boolean,
+  clickToOpenFileName: ?string,
+  clickToOpenLineNumber: ?number
 ) {
   const sourceCode = [];
   let whiteSpace = Infinity;
@@ -83,6 +85,19 @@ function createCode(
   const pre = document.createElement('pre');
   applyStyles(pre, preStyle);
   pre.appendChild(code);
+
+  if (clickToOpenFileName) {
+    pre.style.cursor = 'pointer';
+    pre.addEventListener('click', function() {
+      fetch(
+        '/__open-stack-frame-in-editor?fileName=' +
+          window.encodeURIComponent(clickToOpenFileName) +
+          '&lineNumber=' +
+          window.encodeURIComponent(clickToOpenLineNumber || 1)
+      ).then(() => {}, () => {});
+    });
+  }
+
   return pre;
 }
 
diff --git a/packages/react-error-overlay/src/components/frame.js b/packages/react-error-overlay/src/components/frame.js
index db9812cc3..d5708d720 100644
--- a/packages/react-error-overlay/src/components/frame.js
+++ b/packages/react-error-overlay/src/components/frame.js
@@ -215,7 +215,9 @@ function createFrame(
           lineNumber,
           columnNumber,
           contextSize,
-          critical
+          critical,
+          frame._originalFileName,
+          frame._originalLineNumber
         )
       );
       hasSource = true;
@@ -232,7 +234,9 @@ function createFrame(
           sourceLineNumber,
           sourceColumnNumber,
           contextSize,
-          critical
+          critical,
+          frame._originalFileName,
+          frame._originalLineNumber
         )
       );
       hasSource = true;
diff --git a/packages/react-scripts/scripts/utils/addWebpackMiddleware.js b/packages/react-scripts/scripts/utils/addWebpackMiddleware.js
index a3deaf34a..aec088b80 100644
--- a/packages/react-scripts/scripts/utils/addWebpackMiddleware.js
+++ b/packages/react-scripts/scripts/utils/addWebpackMiddleware.js
@@ -14,6 +14,7 @@ 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');
 
@@ -145,10 +146,23 @@ function registerProxy(devServer, _proxy) {
   });
 }
 
+// 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.
-- 
GitLab