Skip to content

Commit 1766f2b

Browse files
committed
Click to view source
1 parent 76d2d84 commit 1766f2b

File tree

6 files changed

+204
-3
lines changed

6 files changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"react-scripts": "./bin/react-scripts.js"
2222
},
2323
"dependencies": {
24+
"absolute-path": "0.0.0",
2425
"autoprefixer": "6.7.7",
2526
"babel-core": "6.23.1",
2627
"babel-eslint": "7.1.1",
@@ -30,6 +31,7 @@
3031
"babel-runtime": "^6.20.0",
3132
"case-sensitive-paths-webpack-plugin": "1.1.4",
3233
"chalk": "1.1.3",
34+
"child_process": "^1.0.2",
3335
"connect-history-api-fallback": "1.3.0",
3436
"cross-spawn": "4.0.2",
3537
"css-loader": "0.28.0",
@@ -53,6 +55,7 @@
5355
"promise": "7.1.1",
5456
"react-dev-utils": "^0.5.2",
5557
"react-error-overlay": "^0.0.0",
58+
"shell-quote": "^1.6.1",
5659
"style-loader": "0.16.1",
5760
"url-loader": "0.5.8",
5861
"webpack": "2.4.1",

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)