| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 | /** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file at * https://github.com/facebookincubator/create-react-app/blob/master/LICENSE * * Modified by Yuxi Evan You */const fs = require('fs')const os = require('os')const path = require('path')const colors = require('picocolors')const childProcess = require('child_process')const guessEditor = require('./guess')const getArgumentsForPosition = require('./get-args')function wrapErrorCallback (cb) {  return (fileName, errorMessage) => {    console.log()    console.log(      colors.red('Could not open ' + path.basename(fileName) + ' in the editor.')    )    if (errorMessage) {      if (errorMessage[errorMessage.length - 1] !== '.') {        errorMessage += '.'      }      console.log(        colors.red('The editor process exited with an error: ' + errorMessage)      )    }    console.log()    if (cb) cb(fileName, errorMessage)  }}function isTerminalEditor (editor) {  switch (editor) {    case 'vim':    case 'emacs':    case 'nano':      return true  }  return false}const positionRE = /:(\d+)(:(\d+))?$/function parseFile (file) {  // support `file://` protocol  if (file.startsWith('file://')) {    file = require('url').fileURLToPath(file)  }  const fileName = file.replace(positionRE, '')  const match = file.match(positionRE)  const lineNumber = match && match[1]  const columnNumber = match && match[3]  return {    fileName,    lineNumber,    columnNumber  }}let _childProcess = nullfunction launchEditor (file, specifiedEditor, onErrorCallback) {  const parsed = parseFile(file)  let { fileName } = parsed  const { lineNumber, columnNumber } = parsed  if (!fs.existsSync(fileName)) {    return  }  if (typeof specifiedEditor === 'function') {    onErrorCallback = specifiedEditor    specifiedEditor = undefined  }  onErrorCallback = wrapErrorCallback(onErrorCallback)  const [editor, ...args] = guessEditor(specifiedEditor)  if (!editor) {    onErrorCallback(fileName, null)    return  }  if (    process.platform === 'linux' &&    fileName.startsWith('/mnt/') &&    /Microsoft/i.test(os.release())  ) {    // Assume WSL / "Bash on Ubuntu on Windows" is being used, and    // that the file exists on the Windows file system.    // `os.release()` is "4.4.0-43-Microsoft" in the current release    // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364    // When a Windows editor is specified, interop functionality can    // handle the path translation, but only if a relative path is used.    fileName = path.relative('', fileName)  }  if (lineNumber) {    const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)    args.push.apply(args, extraArgs)  } 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, we need to use `exec` with the `shell: true` option,    // and some more sanitization is required.    // However, CMD.exe on Windows is vulnerable to RCE attacks given a file name of the    // form "C:\Users\myusername\Downloads\& curl 172.21.93.52".    // `create-react-app` used a safe file name pattern to validate user-provided file names:    // - https://github.com/facebook/create-react-app/pull/4866    // - https://github.com/facebook/create-react-app/pull/5431    // But that's not a viable solution for this package because    // it's depended on by so many meta frameworks that heavily rely on    // special characters in file names for filesystem-based routing.    // We need to at least:    // - Support `+` because it's used in SvelteKit and Vike    // - Support `$` because it's used in Remix    // - Support `(` and `)` because they are used in Analog, SolidStart, and Vike    // - Support `@` because it's used in Vike    // - Support `[` and `]` because they are widely used for [slug]    // So here we choose to use `^` to escape special characters instead.    // According to https://ss64.com/nt/syntax-esc.html,    // we can use `^` to escape `&`, `<`, `>`, `|`, `%`, and `^`    // I'm not sure if we have to escape all of these, but let's do it anyway    function escapeCmdArgs (cmdArgs) {      return cmdArgs.replace(/([&|<>,;=^])/g, '^$1')    }    // Need to double quote the editor path in case it contains spaces;    // If the fileName contains spaces, we also need to double quote it in the arguments    // However, there's a case that it's concatenated with line number and column number    // which is separated by `:`. We need to double quote the whole string in this case.    // Also, if the string contains the escape character `^`, it needs to be quoted, too.    function doubleQuoteIfNeeded(str) {      if (str.includes('^')) {        // If a string includes an escaped character, not only does it need to be quoted,        // but the quotes need to be escaped too.        return `^"${str}^"`      } else if (str.includes(' ')) {        return `"${str}"`      }       return str    }    const launchCommand = [editor, ...args.map(escapeCmdArgs)]      .map(doubleQuoteIfNeeded)      .join(' ')    _childProcess = childProcess.exec(launchCommand, {      stdio: 'inherit',      shell: true    })  } else {    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })  }  _childProcess.on('exit', function (errorCode) {    _childProcess = null    if (errorCode) {      onErrorCallback(fileName, '(code ' + errorCode + ')')    }  })  _childProcess.on('error', function (error) {    let { code, message } = error    if ('ENOENT' === code) {      message = `${message} ('${editor}' command does not exist in 'PATH')`    }    onErrorCallback(fileName, message);  })}module.exports = launchEditor
 |