From dd9f2345a9805adf5581c326c90b23b5a8dcd58b Mon Sep 17 00:00:00 2001 From: Dan Abramov 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 00000000000..768e883a843 --- /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 8fe7f53b37c..9edb99875c4 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 f25ed83780c..e27e72fd7b3 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 db9812cc30c..d5708d72062 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 a3deaf34a76..aec088b808b 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.