diff --git a/__tests__/app.test.js b/__tests__/app.test.js index 4c4fc0f..b86b4b7 100644 --- a/__tests__/app.test.js +++ b/__tests__/app.test.js @@ -1,5 +1,10 @@ +/* eslint-disable security/detect-child-process */ const TerminalLauncher = require('../index') +const getTerminals = require('../lib/get-terminals') + const opn = require('opn') +const childProcess = require('child_process') + jest.mock('opn') describe('Terminal Launcher', () => { @@ -9,20 +14,81 @@ describe('Terminal Launcher', () => { }).toThrow() }) - test('launching a terminal calls opn with the correct params', async () => { - const path = '/usr/local/bin/non-existent.sh' + describe('Linux', () => { + beforeAll(() => { + Object.defineProperty(process, 'platform', { + value: 'linux' + }) + }) + + test('launching a terminal calls opn with the correct params when running on linux', async () => { + const path = '/usr/local/bin/non-existent.sh' + + await TerminalLauncher.launchTerminal({path}) + expect(opn).toHaveBeenCalled() - await TerminalLauncher.launchTerminal({path}) - expect(opn).toHaveBeenCalled() + const calledWithFirstArgument = opn.mock.calls[0][0] + expect(calledWithFirstArgument).toEqual(path) + }) - const calledWithFirstArgument = opn.mock.calls[0][0] - expect(calledWithFirstArgument).toEqual(path) + describe('get-terminals returns array of functions', () => { + test('terminals are a list of functions', () => { + const terminals = getTerminals('mock.ext') + expect(Array.isArray(terminals)).toBeTruthy() + expect(terminals.length).toBeTruthy() + expect(typeof terminals[0] === 'function').toBeTruthy() + }) + }) }) - test('terminals are a list of functions', () => { - const terminals = TerminalLauncher.getTerminals() - expect(Array.isArray(terminals)).toBeTruthy() - expect(terminals.length).toBeTruthy() - expect(typeof terminals[0] === 'function').toBeTruthy() + describe('Windows', () => { + beforeAll(() => { + Object.defineProperty(process, 'platform', { + value: 'win32' + }) + + Object.defineProperty(process, 'env', { + value: { + PATHEXT: '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC' + } + }) + }) + + beforeEach(() => { + childProcess.exec = jest.fn(() => ({once: (type, c) => type === 'close' && c(0)})) + }) + + describe('launching a terminal calls child_process.exec with the correct command', () => { + test('for powershell scripts', async () => { + const path = 'C:\\script.ps1' + const expectedCommand = 'start powershell "C:\\script.ps1"' + + await TerminalLauncher.launchTerminal({path}) + expect(childProcess.exec).toHaveBeenCalled() + + const calledWithFirstArgument = childProcess.exec.mock.calls[0][0] + expect(calledWithFirstArgument).toEqual(expectedCommand) + }) + + test('for bat scripts', async () => { + const path = 'C:\\script.bat' + const expectedCommand = 'start cmd /c "C:\\script.bat"' + + await TerminalLauncher.launchTerminal({path}) + expect(childProcess.exec).toHaveBeenCalled() + + const calledWithFirstArgument = childProcess.exec.mock.calls[0][0] + expect(calledWithFirstArgument).toEqual(expectedCommand) + }) + }) + + describe('get-terminals returns array of functions', () => { + test('terminals are a list of functions', () => { + const terminals = getTerminals('mock.ext') + expect(Array.isArray(terminals)).toBeTruthy() + expect(terminals.length).toBeTruthy() + expect(typeof terminals[0] === 'function').toBeTruthy() + }) + }) }) }) diff --git a/lib/TerminalLauncher.js b/lib/TerminalLauncher.js index a1b0365..ce73976 100644 --- a/lib/TerminalLauncher.js +++ b/lib/TerminalLauncher.js @@ -1,22 +1,7 @@ 'use strict' -const opn = require('opn') +const getTerminals = require('./get-terminals') const debug = require('debug')('opn-shell') -const terminalAppsInfo = [ - // MacOS variations of terminal apps - 'Hyper', - 'iTerm', - 'terminal.app', - // Linux variations of terminal apps - ['x-terminal-emulator', '-e'], - ['gnome-terminal', '-e'], - ['konsole', '-e'], - ['xterm', '-e'], - ['urxvt', '-e'], - [process.env.COLORTERM, '-e'], - [process.env.XTERM, '-e'] -] - class TerminalLauncher { static launchTerminal({path} = {}) { if (!path) { @@ -24,21 +9,13 @@ class TerminalLauncher { } debug('executing: %s', path) - const shellLauncher = TerminalLauncher.getTerminals(path).reduce((promise, nextPromise) => { - return promise.catch(nextPromise) - }, Promise.reject()) /* eslint prefer-promise-reject-errors: "off" */ - return shellLauncher - } + const shellLauncher = getTerminals(path).reduce( + (promise, nextPromise) => promise.catch(nextPromise), + Promise.reject() + ) /* eslint prefer-promise-reject-errors: "off" */ - static getTerminals(path) { - const terminalApps = terminalAppsInfo.map(appInfo => { - debug('adding terminal configuration: %s', appInfo.toString()) - return () => opn(path, {app: appInfo}) - }) - - terminalApps.push(() => opn(path)) - return terminalApps + return shellLauncher } } diff --git a/lib/get-terminals-linux.js b/lib/get-terminals-linux.js new file mode 100644 index 0000000..6f8baa7 --- /dev/null +++ b/lib/get-terminals-linux.js @@ -0,0 +1,31 @@ +'use strict' + +const opn = require('opn') +const debug = require('debug')('opn-shell') + +function getLinuxTerminalHandlers(path) { + const terminalApps = [ + // MacOS variations of terminal apps + 'Hyper', + 'iTerm', + 'terminal.app', + // Linux variations of terminal apps + ['x-terminal-emulator', '-e'], + ['gnome-terminal', '-e'], + ['konsole', '-e'], + ['xterm', '-e'], + ['urxvt', '-e'], + [process.env.COLORTERM, '-e'], + [process.env.XTERM, '-e'] + ] + + const handlers = terminalApps.map(appInfo => { + debug('adding terminal configuration: %s', appInfo.toString()) + return () => opn(path, {app: appInfo}) + }) + + handlers.push(() => opn(path)) + return handlers +} + +module.exports = getLinuxTerminalHandlers diff --git a/lib/get-terminals-windows.js b/lib/get-terminals-windows.js new file mode 100644 index 0000000..0a3c1d9 --- /dev/null +++ b/lib/get-terminals-windows.js @@ -0,0 +1,42 @@ +/* eslint-disable security/detect-child-process */ +'use strict' + +const debug = require('debug')('opn-shell') +const path = require('path') +const childProcess = require('child_process') + +function getWindowsTerminalHandlers(scriptPath) { + const extension = path.extname(scriptPath).toLowerCase() + const logAddedTerminal = terminal => debug(`'adding terminal configuration: ${terminal}'`) + + let command + if (getCmdSupportedExtensions().includes(extension)) { + command = `start cmd /c "${scriptPath}"` + logAddedTerminal('cmd') + } else { + command = `start powershell "${scriptPath}"` + logAddedTerminal('powershell') + } + + const handler = () => + new Promise((resolve, reject) => { + const cp = childProcess.exec(command) + + cp.once('error', reject) + cp.once( + 'close', + code => (code === 0 ? resolve(cp) : reject(new Error('Exited with code ' + code))) + ) + }) + + return [handler] +} + +function getCmdSupportedExtensions() { + // PATHEXT contains semicolon separated list of file extensions which are considered to be executable by Windows + // (runnable by cmd) + const extensions = process.env.PATHEXT.toLowerCase().split(';') + return extensions +} + +module.exports = getWindowsTerminalHandlers diff --git a/lib/get-terminals.js b/lib/get-terminals.js new file mode 100644 index 0000000..51d6910 --- /dev/null +++ b/lib/get-terminals.js @@ -0,0 +1,10 @@ +'use strict' + +function getTerminals(executablePath) { + const module = /^win/i.test(process.platform) + ? './get-terminals-windows' + : './get-terminals-linux' + return require(module)(executablePath) +} + +module.exports = getTerminals