main
js 194 lines 5.53 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import {existsSync} from 'fs';
11 import {basename, join, isAbsolute} from 'path';
12 import {execSync, spawn} from 'child_process';
13 import {parse} from 'shell-quote';
14
15 function isTerminalEditor(editor: string): boolean {
16 switch (editor) {
17 case 'vim':
18 case 'emacs':
19 case 'nano':
20 return true;
21 default:
22 return false;
23 }
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 const COMMON_EDITORS = {
30 '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
31 '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
32 '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
33 '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
34 '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
35 '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
36 '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
37 '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
38 };
39
40 function getArgumentsForLineNumber(
41 editor: string,
42 filePath: string,
43 lineNumber: number,
44 ): Array<string> {
45 switch (basename(editor)) {
46 case 'vim':
47 case 'mvim':
48 return [filePath, '+' + 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 [filePath + ':' + lineNumber];
59 case 'joe':
60 case 'emacs':
61 case 'emacsclient':
62 return ['+' + lineNumber, filePath];
63 case 'rmate':
64 case 'mate':
65 case 'mine':
66 return ['--line', lineNumber + '', filePath];
67 case 'code':
68 return ['-g', filePath + ':' + lineNumber];
69 default:
70 // For all others, drop the lineNumber until we have
71 // a mapping above, since providing the lineNumber incorrectly
72 // can result in errors or confusing behavior.
73 return [filePath];
74 }
75 }
76
77 function guessEditor(): Array<string> {
78 // Explicit config always wins
79 if (process.env.REACT_EDITOR) {
80 return parse(process.env.REACT_EDITOR);
81 }
82
83 // Using `ps x` on OSX we can find out which editor is currently running.
84 // Potentially we could use similar technique for Windows and Linux
85 if (process.platform === 'darwin') {
86 try {
87 const output = execSync('ps x').toString();
88 const processNames = Object.keys(COMMON_EDITORS);
89 for (let i = 0; i < processNames.length; i++) {
90 const processName = processNames[i];
91 if (output.indexOf(processName) !== -1) {
92 return [COMMON_EDITORS[processName]];
93 }
94 }
95 } catch (error) {
96 // Ignore...
97 }
98 }
99
100 // Last resort, use old-school env vars
101 if (process.env.VISUAL) {
102 return [process.env.VISUAL];
103 } else if (process.env.EDITOR) {
104 return [process.env.EDITOR];
105 }
106
107 return [];
108 }
109
110 let childProcess = null;
111
112 export function getValidFilePath(
113 maybeRelativePath: string,
114 absoluteProjectRoots: Array<string>,
115 ): string | null {
116 // We use relative paths at Facebook with deterministic builds.
117 // This is why our internal tooling calls React DevTools with absoluteProjectRoots.
118 // If the filename is absolute then we don't need to care about this.
119 if (isAbsolute(maybeRelativePath)) {
120 if (existsSync(maybeRelativePath)) {
121 return maybeRelativePath;
122 }
123 } else {
124 for (let i = 0; i < absoluteProjectRoots.length; i++) {
125 const projectRoot = absoluteProjectRoots[i];
126 const joinedPath = join(projectRoot, maybeRelativePath);
127 if (existsSync(joinedPath)) {
128 return joinedPath;
129 }
130 }
131 }
132
133 return null;
134 }
135
136 export function doesFilePathExist(
137 maybeRelativePath: string,
138 absoluteProjectRoots: Array<string>,
139 ): boolean {
140 return getValidFilePath(maybeRelativePath, absoluteProjectRoots) !== null;
141 }
142
143 export function launchEditor(
144 maybeRelativePath: string,
145 lineNumber: number,
146 absoluteProjectRoots: Array<string>,
147 ) {
148 const filePath = getValidFilePath(maybeRelativePath, absoluteProjectRoots);
149 if (filePath === null) {
150 return;
151 }
152
153 // Sanitize lineNumber to prevent malicious use on win32
154 // via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
155 if (lineNumber && isNaN(lineNumber)) {
156 return;
157 }
158
159 const [editor, ...destructuredArgs] = guessEditor();
160 if (!editor) {
161 return;
162 }
163
164 let args = destructuredArgs;
165
166 if (lineNumber) {
167 args = args.concat(getArgumentsForLineNumber(editor, filePath, lineNumber));
168 } else {
169 args.push(filePath);
170 }
171
172 if (childProcess && isTerminalEditor(editor)) {
173 // There's an existing editor process already and it's attached
174 // to the terminal, so go kill it. Otherwise two separate editor
175 // instances attach to the stdin/stdout which gets confusing.
176 // $FlowFixMe[incompatible-use] found when upgrading Flow
177 childProcess.kill('SIGKILL');
178 }
179
180 if (process.platform === 'win32') {
181 // On Windows, launch the editor in a shell because spawn can only
182 // launch .exe files.
183 childProcess = spawn('cmd.exe', ['/C', editor].concat(args), {
184 stdio: 'inherit',
185 });
186 } else {
187 childProcess = spawn(editor, args, {stdio: 'inherit'});
188 }
189 childProcess.on('error', function () {});
190 // $FlowFixMe[incompatible-use] found when upgrading Flow
191 childProcess.on('exit', function () {
192 childProcess = null;
193 });
194 }