From 1766f2bd28c44b43bbe14409b2b8de37435b8819 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 13 May 2017 21:06:36 +0100 Subject: [PATCH 1/4] Click to view source --- packages/react-dev-utils/launchEditor.js | 163 ++++++++++++++++++ packages/react-dev-utils/package.json | 2 + .../src/components/code.js | 17 +- .../src/components/frame.js | 8 +- packages/react-scripts/package.json | 3 + .../scripts/utils/addWebpackMiddleware.js | 14 ++ 6 files changed, 204 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..2764b18fd86 --- /dev/null +++ b/packages/react-dev-utils/launchEditor.js @@ -0,0 +1,163 @@ +/** + * 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 chalk = require('chalk'); +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, projectRoots) { + 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(errorCode) { + _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/package.json b/packages/react-scripts/package.json index 86961ff905a..862ab9d5d5d 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -21,6 +21,7 @@ "react-scripts": "./bin/react-scripts.js" }, "dependencies": { + "absolute-path": "0.0.0", "autoprefixer": "6.7.7", "babel-core": "6.23.1", "babel-eslint": "7.1.1", @@ -30,6 +31,7 @@ "babel-runtime": "^6.20.0", "case-sensitive-paths-webpack-plugin": "1.1.4", "chalk": "1.1.3", + "child_process": "^1.0.2", "connect-history-api-fallback": "1.3.0", "cross-spawn": "4.0.2", "css-loader": "0.28.0", @@ -53,6 +55,7 @@ "promise": "7.1.1", "react-dev-utils": "^0.5.2", "react-error-overlay": "^0.0.0", + "shell-quote": "^1.6.1", "style-loader": "0.16.1", "url-loader": "0.5.8", "webpack": "2.4.1", 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. From ee02ef32ddb0754709160c4e728fb63294e33b00 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 14 May 2017 03:20:51 +0100 Subject: [PATCH 2/4] Update package.json --- packages/react-scripts/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 862ab9d5d5d..a058e5a16cc 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -21,7 +21,6 @@ "react-scripts": "./bin/react-scripts.js" }, "dependencies": { - "absolute-path": "0.0.0", "autoprefixer": "6.7.7", "babel-core": "6.23.1", "babel-eslint": "7.1.1", @@ -31,7 +30,6 @@ "babel-runtime": "^6.20.0", "case-sensitive-paths-webpack-plugin": "1.1.4", "chalk": "1.1.3", - "child_process": "^1.0.2", "connect-history-api-fallback": "1.3.0", "cross-spawn": "4.0.2", "css-loader": "0.28.0", From f43881eb48a20a6fddccb1b59aa829a4a85fc33c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 14 May 2017 03:21:08 +0100 Subject: [PATCH 3/4] Update package.json --- packages/react-scripts/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index a058e5a16cc..86961ff905a 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -53,7 +53,6 @@ "promise": "7.1.1", "react-dev-utils": "^0.5.2", "react-error-overlay": "^0.0.0", - "shell-quote": "^1.6.1", "style-loader": "0.16.1", "url-loader": "0.5.8", "webpack": "2.4.1", From 2cc214a5543b198bdba0cb26553faa9bcfd52014 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 14 May 2017 12:15:53 +0100 Subject: [PATCH 4/4] Fix lint --- packages/react-dev-utils/launchEditor.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-dev-utils/launchEditor.js b/packages/react-dev-utils/launchEditor.js index 2764b18fd86..768e883a843 100644 --- a/packages/react-dev-utils/launchEditor.js +++ b/packages/react-dev-utils/launchEditor.js @@ -8,7 +8,6 @@ */ 'use strict'; -var chalk = require('chalk'); var fs = require('fs'); var path = require('path'); var child_process = require('child_process'); @@ -112,7 +111,7 @@ function guessEditor() { } var _childProcess = null; -function launchEditor(fileName, lineNumber, projectRoots) { +function launchEditor(fileName, lineNumber) { if (!fs.existsSync(fileName)) { return; } @@ -155,7 +154,7 @@ function launchEditor(fileName, lineNumber, projectRoots) { } else { _childProcess = child_process.spawn(editor, args, { stdio: 'inherit' }); } - _childProcess.on('exit', function(errorCode) { + _childProcess.on('exit', function() { _childProcess = null; }); }