diff --git a/index.d.ts b/index.d.ts index 208e9c1..9c8f2a3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -34,11 +34,13 @@ declare namespace open { readonly newInstance?: boolean; /** - Specify the `name` of the app to open the `target` with and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. + Specify the `name` of the app to open the `target` with, and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. + + The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. */ readonly app?: App | readonly App[]; @@ -52,6 +54,15 @@ declare namespace open { readonly allowNonzeroExitCode?: boolean; } + interface OpenAppOptions extends Omit { + /** + Arguments passed to the app. + + These arguments are app dependent. Check the app's documentation for what arguments it accepts. + */ + readonly arguments?: readonly string[]; + } + type AppName = | 'chrome' | 'firefox' @@ -113,6 +124,30 @@ declare const open: { ``` */ apps: Record; + + /** + Open an app. Cross-platform. + + Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. + + @param name - The app you want to open. Can be either builtin supported `open.apps` names or other name supported in platform. + @returns The [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. + + @example + ``` + const {apps, openApp} = require('open'); + + // Open Firefox + await openApp(apps.firefox); + + // Open Chrome incognito mode + await openApp(apps.chrome, {arguments: ['--incognito']}); + + // Open Xcode + await openApp('xcode'); + ``` + */ + openApp: (name: open.App['name'], options?: open.OpenAppOptions) => Promise; }; export = open; diff --git a/index.js b/index.js index 1574a93..290b4ec 100644 --- a/index.js +++ b/index.js @@ -69,11 +69,7 @@ const pTryEach = async (array, mapper) => { throw latestError; }; -const open = async (target, options) => { - if (typeof target !== 'string') { - throw new TypeError('Expected a `target`'); - } - +const baseOpen = async options => { options = { wait: false, background: false, @@ -83,7 +79,7 @@ const open = async (target, options) => { }; if (Array.isArray(options.app)) { - return pTryEach(options.app, singleApp => open(target, { + return pTryEach(options.app, singleApp => baseOpen({ ...options, app: singleApp })); @@ -93,7 +89,7 @@ const open = async (target, options) => { appArguments = [...appArguments]; if (Array.isArray(app)) { - return pTryEach(app, appName => open(target, { + return pTryEach(app, appName => baseOpen({ ...options, app: { name: appName, @@ -153,9 +149,11 @@ const open = async (target, options) => { // Double quote with double quotes to ensure the inner quotes are passed through. // Inner quotes are delimited for PowerShell interpretation with backticks. encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList'); - appArguments.unshift(target); - } else { - encodedArguments.push(`"${target}"`); + if (options.target) { + appArguments.unshift(options.target); + } + } else if (options.target) { + encodedArguments.push(`"${options.target}"`); } if (appArguments.length > 0) { @@ -164,7 +162,7 @@ const open = async (target, options) => { } // Using Base64-encoded command, accepted by PowerShell, to allow special characters. - target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); + options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); } else { if (app) { command = app; @@ -196,7 +194,9 @@ const open = async (target, options) => { } } - cliArguments.push(target); + if (options.target) { + cliArguments.push(options.target); + } if (platform === 'darwin' && appArguments.length > 0) { cliArguments.push('--args', ...appArguments); @@ -224,6 +224,36 @@ const open = async (target, options) => { return subprocess; }; +const open = (target, options) => { + if (typeof target !== 'string') { + throw new TypeError('Expected a `target`'); + } + + return baseOpen({ + ...options, + target + }); +}; + +const openApp = (name, options) => { + if (typeof name !== 'string') { + throw new TypeError('Expected a `name`'); + } + + const {arguments: appArguments = []} = options || {}; + if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) { + throw new TypeError('Expected `appArguments` as Array type'); + } + + return baseOpen({ + ...options, + app: { + name, + arguments: appArguments + } + }); +}; + function detectArchBinary(binary) { if (typeof binary === 'string' || Array.isArray(binary)) { return binary; @@ -255,7 +285,7 @@ const apps = {}; defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({ darwin: 'google chrome', win32: 'chrome', - linux: ['google-chrome', 'google-chrome-stable'] + linux: ['google-chrome', 'google-chrome-stable', 'chromium'] }, { wsl: { ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', @@ -274,11 +304,12 @@ defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({ defineLazyProperty(apps, 'edge', () => detectPlatformBinary({ darwin: 'microsoft edge', win32: 'msedge', - linux: 'microsoft-edge' + linux: ['microsoft-edge', 'microsoft-edge-dev'] }, { wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' })); open.apps = apps; +open.openApp = openApp; module.exports = open; diff --git a/package.json b/package.json index c14b814..0b8bb25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open", - "version": "8.2.1", + "version": "8.4.0", "description": "Open stuff like URLs, files, executables. Cross-platform.", "license": "MIT", "repository": "sindresorhus/open", diff --git a/readme.md b/readme.md index b2d2ebb..18794a2 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ This is meant to be used in command-line tools and scripts, not in the browser. If you need this for Electron, use [`shell.openPath()`](https://www.electronjs.org/docs/api/shell#shellopenpathpath) instead. -Note: The original [`open` package](https://github.com/pwnall/node-open) was previously deprecated in favor of this package, and we got the name, so this package is now named `open` instead of `opn`. If you're upgrading from the original `open` package (`open@0.0.5` or lower), keep in mind that the API is different. +This package does not make any security guarantees. If you pass in untrusted input, it's up to you to properly sanitize it. #### Why? @@ -40,6 +40,12 @@ await open('https://sindresorhus.com', {app: {name: 'firefox'}}); // Specify app arguments. await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}}); + +// Open an app +await open.openApp('xcode'); + +// Open an app with arguments +await open.openApp(open.apps.chrome, {arguments: ['--incognito']}); ``` ## API @@ -93,12 +99,14 @@ A new instance is always opened on other platforms. Type: `{name: string | string[], arguments?: string[]} | Array<{name: string | string[], arguments: string[]}>` -Specify the `name` of the app to open the `target` with and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. +Specify the `name` of the app to open the `target` with, and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. +The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. + ##### allowNonzeroExitCode Type: `boolean`\ @@ -128,6 +136,35 @@ await open('https://google.com', { - [`firefox`](https://www.mozilla.org/firefox) - Web browser - [`edge`](https://www.microsoft.com/edge) - Web browser +### open.openApp(name, options?) + +Open an app. + +Returns a promise for the [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. + +#### name + +Type: `string` + +The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. + +You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. + +#### options + +Type: `object` + +Same options as [`open`](#options) except `app` and with the following additions: + +##### arguments + +Type: `string[]`\ +Default: `[]` + +Arguments passed to the app. + +These arguments are app dependent. Check the app's documentation for what arguments it accepts. + ## Related - [open-cli](https://github.com/sindresorhus/open-cli) - CLI for this module diff --git a/test.js b/test.js index 447e5df..2fe1c38 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ const test = require('ava'); const open = require('.'); +const {openApp} = open; // Tests only checks that opening doesn't return an error // it has no way make sure that it actually opened anything. @@ -69,3 +70,11 @@ test('open URL with query strings and URL reserved characters', async t => { test('open URL with query strings and URL reserved characters with `url` option', async t => { await t.notThrowsAsync(open('https://httpbin.org/get?amp=%26&colon=%3A&comma=%2C&commat=%40&dollar=%24&equals=%3D&plus=%2B&quest=%3F&semi=%3B&sol=%2F', {url: true})); }); + +test('open Firefox without arguments', async t => { + await t.notThrowsAsync(openApp(open.apps.firefox)); +}); + +test('open Chrome in incognito mode', async t => { + await t.notThrowsAsync(openApp(open.apps.chrome, {arguments: ['--incognito'], newInstance: true})); +});