Skip to content

Commit dd9f234

Browse files
gaearonromaindso
authored andcommitted
Click to view source from error overlay (facebook#2141)
* Click to view source * Update package.json * Update package.json * Fix lint
1 parent 24ed2cc commit dd9f234

File tree

5 files changed

+200
-3
lines changed

5 files changed

+200
-3
lines changed
+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
'use strict';
10+
11+
var fs = require('fs');
12+
var path = require('path');
13+
var child_process = require('child_process');
14+
const shellQuote = require('shell-quote');
15+
16+
function isTerminalEditor(editor) {
17+
switch (editor) {
18+
case 'vim':
19+
case 'emacs':
20+
case 'nano':
21+
return true;
22+
}
23+
return false;
24+
}
25+
26+
// Map from full process name to binary that starts the process
27+
// We can't just re-use full process name, because it will spawn a new instance
28+
// of the app every time
29+
var COMMON_EDITORS = {
30+
'/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
31+
'/Applications/Atom Beta.app/Contents/MacOS/Atom Beta': '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
32+
'/Applications/Sublime Text.app/Contents/MacOS/Sublime Text': '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
33+
'/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2': '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
34+
'/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
35+
};
36+
37+
function addWorkspaceToArgumentsIfExists(args, workspace) {
38+
if (workspace) {
39+
args.unshift(workspace);
40+
}
41+
return args;
42+
}
43+
44+
function getArgumentsForLineNumber(editor, fileName, lineNumber, workspace) {
45+
switch (path.basename(editor)) {
46+
case 'vim':
47+
case 'mvim':
48+
return [fileName, '+' + lineNumber];
49+
case 'atom':
50+
case 'Atom':
51+
case 'Atom Beta':
52+
case 'subl':
53+
case 'sublime':
54+
case 'wstorm':
55+
case 'appcode':
56+
case 'charm':
57+
case 'idea':
58+
return [fileName + ':' + lineNumber];
59+
case 'joe':
60+
case 'emacs':
61+
case 'emacsclient':
62+
return ['+' + lineNumber, fileName];
63+
case 'rmate':
64+
case 'mate':
65+
case 'mine':
66+
return ['--line', lineNumber, fileName];
67+
case 'code':
68+
return addWorkspaceToArgumentsIfExists(
69+
['-g', fileName + ':' + lineNumber],
70+
workspace
71+
);
72+
}
73+
74+
// For all others, drop the lineNumber until we have
75+
// a mapping above, since providing the lineNumber incorrectly
76+
// can result in errors or confusing behavior.
77+
return [fileName];
78+
}
79+
80+
function guessEditor() {
81+
// Explicit config always wins
82+
if (process.env.REACT_EDITOR) {
83+
return shellQuote.parse(process.env.REACT_EDITOR);
84+
}
85+
86+
// Using `ps x` on OSX we can find out which editor is currently running.
87+
// Potentially we could use similar technique for Windows and Linux
88+
if (process.platform === 'darwin') {
89+
try {
90+
var output = child_process.execSync('ps x').toString();
91+
var processNames = Object.keys(COMMON_EDITORS);
92+
for (var i = 0; i < processNames.length; i++) {
93+
var processName = processNames[i];
94+
if (output.indexOf(processName) !== -1) {
95+
return [COMMON_EDITORS[processName]];
96+
}
97+
}
98+
} catch (error) {
99+
// Ignore...
100+
}
101+
}
102+
103+
// Last resort, use old skool env vars
104+
if (process.env.VISUAL) {
105+
return [process.env.VISUAL];
106+
} else if (process.env.EDITOR) {
107+
return [process.env.EDITOR];
108+
}
109+
110+
return [null];
111+
}
112+
113+
var _childProcess = null;
114+
function launchEditor(fileName, lineNumber) {
115+
if (!fs.existsSync(fileName)) {
116+
return;
117+
}
118+
119+
// Sanitize lineNumber to prevent malicious use on win32
120+
// via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
121+
if (lineNumber && isNaN(lineNumber)) {
122+
return;
123+
}
124+
125+
let [editor, ...args] = guessEditor();
126+
if (!editor) {
127+
return;
128+
}
129+
130+
var workspace = null;
131+
if (lineNumber) {
132+
args = args.concat(
133+
getArgumentsForLineNumber(editor, fileName, lineNumber, workspace)
134+
);
135+
} else {
136+
args.push(fileName);
137+
}
138+
139+
if (_childProcess && isTerminalEditor(editor)) {
140+
// There's an existing editor process already and it's attached
141+
// to the terminal, so go kill it. Otherwise two separate editor
142+
// instances attach to the stdin/stdout which gets confusing.
143+
_childProcess.kill('SIGKILL');
144+
}
145+
146+
if (process.platform === 'win32') {
147+
// On Windows, launch the editor in a shell because spawn can only
148+
// launch .exe files.
149+
_childProcess = child_process.spawn(
150+
'cmd.exe',
151+
['/C', editor].concat(args),
152+
{ stdio: 'inherit' }
153+
);
154+
} else {
155+
_childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
156+
}
157+
_childProcess.on('exit', function() {
158+
_childProcess = null;
159+
});
160+
}
161+
162+
module.exports = launchEditor;

packages/react-dev-utils/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"formatWebpackMessages.js",
2020
"getProcessForPort.js",
2121
"InterpolateHtmlPlugin.js",
22+
"launchEditor.js",
2223
"openBrowser.js",
2324
"openChrome.applescript",
2425
"prompt.js",
@@ -35,6 +36,7 @@
3536
"html-entities": "1.2.0",
3637
"opn": "4.0.2",
3738
"recursive-readdir": "2.1.1",
39+
"shell-quote": "^1.6.1",
3840
"sockjs-client": "1.1.2",
3941
"stack-frame-mapper": "0.4.0",
4042
"stack-frame-parser": "0.4.0",

packages/react-error-overlay/src/components/code.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ function createCode(
1919
lineNum: number,
2020
columnNum: number | null,
2121
contextSize: number,
22-
main: boolean = false
22+
main: boolean,
23+
clickToOpenFileName: ?string,
24+
clickToOpenLineNumber: ?number
2325
) {
2426
const sourceCode = [];
2527
let whiteSpace = Infinity;
@@ -83,6 +85,19 @@ function createCode(
8385
const pre = document.createElement('pre');
8486
applyStyles(pre, preStyle);
8587
pre.appendChild(code);
88+
89+
if (clickToOpenFileName) {
90+
pre.style.cursor = 'pointer';
91+
pre.addEventListener('click', function() {
92+
fetch(
93+
'/__open-stack-frame-in-editor?fileName=' +
94+
window.encodeURIComponent(clickToOpenFileName) +
95+
'&lineNumber=' +
96+
window.encodeURIComponent(clickToOpenLineNumber || 1)
97+
).then(() => {}, () => {});
98+
});
99+
}
100+
86101
return pre;
87102
}
88103

packages/react-error-overlay/src/components/frame.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,9 @@ function createFrame(
215215
lineNumber,
216216
columnNumber,
217217
contextSize,
218-
critical
218+
critical,
219+
frame._originalFileName,
220+
frame._originalLineNumber
219221
)
220222
);
221223
hasSource = true;
@@ -232,7 +234,9 @@ function createFrame(
232234
sourceLineNumber,
233235
sourceColumnNumber,
234236
contextSize,
235-
critical
237+
critical,
238+
frame._originalFileName,
239+
frame._originalLineNumber
236240
)
237241
);
238242
hasSource = true;

packages/react-scripts/scripts/utils/addWebpackMiddleware.js

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const chalk = require('chalk');
1414
const dns = require('dns');
1515
const historyApiFallback = require('connect-history-api-fallback');
1616
const httpProxyMiddleware = require('http-proxy-middleware');
17+
const launchEditor = require('react-dev-utils/launchEditor');
1718
const url = require('url');
1819
const paths = require('../../config/paths');
1920

@@ -145,10 +146,23 @@ function registerProxy(devServer, _proxy) {
145146
});
146147
}
147148

149+
// This is used by the crash overlay.
150+
function launchEditorMiddleware() {
151+
return function(req, res, next) {
152+
if (req.url.startsWith('/__open-stack-frame-in-editor')) {
153+
launchEditor(req.query.fileName, req.query.lineNumber);
154+
res.end();
155+
} else {
156+
next();
157+
}
158+
};
159+
}
160+
148161
module.exports = function addWebpackMiddleware(devServer) {
149162
// `proxy` lets you to specify a fallback server during development.
150163
// Every unrecognized request will be forwarded to it.
151164
const proxy = require(paths.appPackageJson).proxy;
165+
devServer.use(launchEditorMiddleware());
152166
devServer.use(
153167
historyApiFallback({
154168
// Paths with dots should still use the history fallback.

0 commit comments

Comments
 (0)