Skip to content

Commit 1acc682

Browse files
Kudosindresorhus
andauthoredOct 7, 2021
Add openApp method (#263)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent e21e623 commit 1acc682

File tree

4 files changed

+120
-12
lines changed

4 files changed

+120
-12
lines changed
 

‎index.d.ts

+33
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ declare namespace open {
5454
readonly allowNonzeroExitCode?: boolean;
5555
}
5656

57+
interface OpenAppOptions extends Omit<Options, 'app'> {
58+
/**
59+
Arguments passed to the app.
60+
61+
These arguments are app dependent. Check the app's documentation for what arguments it accepts.
62+
*/
63+
readonly arguments?: readonly string[];
64+
}
65+
5766
type AppName =
5867
| 'chrome'
5968
| 'firefox'
@@ -115,6 +124,30 @@ declare const open: {
115124
```
116125
*/
117126
apps: Record<open.AppName, string | readonly string[]>;
127+
128+
/**
129+
Open an app. Cross-platform.
130+
131+
Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms.
132+
133+
@param name - The app you want to open. Can be either builtin supported `open.apps` names or other name supported in platform.
134+
@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.
135+
136+
@example
137+
```
138+
const {apps, openApp} = require('open');
139+
140+
// Open Firefox
141+
await openApp(apps.firefox);
142+
143+
// Open Chrome incognito mode
144+
await openApp(apps.chrome, {arguments: ['--incognito']});
145+
146+
// Open Xcode
147+
await openApp('xcode');
148+
```
149+
*/
150+
openApp: (name: open.App['name'], options?: open.OpenAppOptions) => Promise<ChildProcess>;
118151
};
119152

120153
export = open;

‎index.js

+43-12
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ const pTryEach = async (array, mapper) => {
6969
throw latestError;
7070
};
7171

72-
const open = async (target, options) => {
73-
if (typeof target !== 'string') {
74-
throw new TypeError('Expected a `target`');
75-
}
76-
72+
const baseOpen = async options => {
7773
options = {
7874
wait: false,
7975
background: false,
@@ -83,7 +79,7 @@ const open = async (target, options) => {
8379
};
8480

8581
if (Array.isArray(options.app)) {
86-
return pTryEach(options.app, singleApp => open(target, {
82+
return pTryEach(options.app, singleApp => baseOpen({
8783
...options,
8884
app: singleApp
8985
}));
@@ -93,7 +89,7 @@ const open = async (target, options) => {
9389
appArguments = [...appArguments];
9490

9591
if (Array.isArray(app)) {
96-
return pTryEach(app, appName => open(target, {
92+
return pTryEach(app, appName => baseOpen({
9793
...options,
9894
app: {
9995
name: appName,
@@ -153,9 +149,11 @@ const open = async (target, options) => {
153149
// Double quote with double quotes to ensure the inner quotes are passed through.
154150
// Inner quotes are delimited for PowerShell interpretation with backticks.
155151
encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList');
156-
appArguments.unshift(target);
157-
} else {
158-
encodedArguments.push(`"${target}"`);
152+
if (options.target) {
153+
appArguments.unshift(options.target);
154+
}
155+
} else if (options.target) {
156+
encodedArguments.push(`"${options.target}"`);
159157
}
160158

161159
if (appArguments.length > 0) {
@@ -164,7 +162,7 @@ const open = async (target, options) => {
164162
}
165163

166164
// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
167-
target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
165+
options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
168166
} else {
169167
if (app) {
170168
command = app;
@@ -196,7 +194,9 @@ const open = async (target, options) => {
196194
}
197195
}
198196

199-
cliArguments.push(target);
197+
if (options.target) {
198+
cliArguments.push(options.target);
199+
}
200200

201201
if (platform === 'darwin' && appArguments.length > 0) {
202202
cliArguments.push('--args', ...appArguments);
@@ -224,6 +224,36 @@ const open = async (target, options) => {
224224
return subprocess;
225225
};
226226

227+
const open = (target, options) => {
228+
if (typeof target !== 'string') {
229+
throw new TypeError('Expected a `target`');
230+
}
231+
232+
return baseOpen({
233+
...options,
234+
target
235+
});
236+
};
237+
238+
const openApp = (name, options) => {
239+
if (typeof name !== 'string') {
240+
throw new TypeError('Expected a `name`');
241+
}
242+
243+
const {arguments: appArguments = []} = options || {};
244+
if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
245+
throw new TypeError('Expected `appArguments` as Array type');
246+
}
247+
248+
return baseOpen({
249+
...options,
250+
app: {
251+
name,
252+
arguments: appArguments
253+
}
254+
});
255+
};
256+
227257
function detectArchBinary(binary) {
228258
if (typeof binary === 'string' || Array.isArray(binary)) {
229259
return binary;
@@ -280,5 +310,6 @@ defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
280310
}));
281311

282312
open.apps = apps;
313+
open.openApp = openApp;
283314

284315
module.exports = open;

‎readme.md

+35
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ await open('https://sindresorhus.com', {app: {name: 'firefox'}});
4040

4141
// Specify app arguments.
4242
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});
43+
44+
// Open an app
45+
await open.openApp('xcode');
46+
47+
// Open an app with arguments
48+
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});
4349
```
4450

4551
## API
@@ -130,6 +136,35 @@ await open('https://google.com', {
130136
- [`firefox`](https://www.mozilla.org/firefox) - Web browser
131137
- [`edge`](https://www.microsoft.com/edge) - Web browser
132138

139+
### open.openApp(name, options?)
140+
141+
Open an app.
142+
143+
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.
144+
145+
#### name
146+
147+
Type: `string`
148+
149+
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.
150+
151+
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.
152+
153+
#### options
154+
155+
Type: `object`
156+
157+
Same options as [`open`](#options) except `app` and with the following additions:
158+
159+
##### arguments
160+
161+
Type: `string[]`\
162+
Default: `[]`
163+
164+
Arguments passed to the app.
165+
166+
These arguments are app dependent. Check the app's documentation for what arguments it accepts.
167+
133168
## Related
134169

135170
- [open-cli](https://github.com/sindresorhus/open-cli) - CLI for this module

‎test.js

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const test = require('ava');
22
const open = require('.');
3+
const {openApp} = open;
34

45
// Tests only checks that opening doesn't return an error
56
// 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 => {
6970
test('open URL with query strings and URL reserved characters with `url` option', async t => {
7071
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}));
7172
});
73+
74+
test('open Firefox without arguments', async t => {
75+
await t.notThrowsAsync(openApp(open.apps.firefox));
76+
});
77+
78+
test('open Chrome in incognito mode', async t => {
79+
await t.notThrowsAsync(openApp(open.apps.chrome, {arguments: ['--incognito'], newInstance: true}));
80+
});

0 commit comments

Comments
 (0)
Please sign in to comment.