Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add the ability to open default browser and default browser in privat…
…e mode (#294)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
leslieyip02 and sindresorhus committed Mar 20, 2023
1 parent cbc008b commit 3b79981
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 43 deletions.
62 changes: 40 additions & 22 deletions index.d.ts
Expand Up @@ -66,14 +66,32 @@ declare namespace open {
type AppName =
| 'chrome'
| 'firefox'
| 'edge';
| 'edge'
| 'browser'
| 'browserPrivate';

type App = {
name: string | readonly string[];
arguments?: readonly string[];
};
}

/**
An object containing auto-detected binary names for common apps. Useful to work around cross-platform differences.
@example
```
import open from 'open';
await open('https://google.com', {
app: {
name: open.apps.chrome
}
});
```
*/
declare const apps: Record<open.AppName, string | readonly string[]>;

// eslint-disable-next-line no-redeclare
declare const open: {
/**
Expand All @@ -88,20 +106,23 @@ declare const open: {
@example
```
import open = require('open');
import open from 'open';
// Opens the image in the default image viewer
// Opens the image in the default image viewer.
await open('unicorn.png', {wait: true});
console.log('The image viewer app closed');
console.log('The image viewer app quit');
// Opens the url in the default browser
// Opens the URL in the default browser.
await open('https://sindresorhus.com');
// Opens the URL in a specified browser.
await open('https://sindresorhus.com', {app: {name: 'firefox'}});
// Specify app arguments.
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});
// Opens the URL in the default browser in incognito mode.
await open('https://sindresorhus.com', {app: {name: open.apps.browserPrivate}});
```
*/
(
Expand All @@ -111,19 +132,8 @@ declare const open: {

/**
An object containing auto-detected binary names for common apps. Useful to work around cross-platform differences.
@example
```
import open = require('open');
await open('https://google.com', {
app: {
name: open.apps.chrome
}
});
```
*/
apps: Record<open.AppName, string | readonly string[]>;
apps: typeof apps;

/**
Open an app. Cross-platform.
Expand All @@ -135,19 +145,27 @@ declare const open: {
@example
```
const {apps, openApp} = require('open');
import open from 'open';
const {apps, openApp} = open;
// Open Firefox
// Open Firefox.
await openApp(apps.firefox);
// Open Chrome incognito mode
// Open Chrome in incognito mode.
await openApp(apps.chrome, {arguments: ['--incognito']});
// Open Xcode
// Open default browser.
await openApp(apps.browser);
// Open default browser in incognito mode.
await openApp(apps.browserPrivate);
// Open Xcode.
await openApp('xcode');
```
*/
openApp: (name: open.App['name'], options?: open.OpenAppOptions) => Promise<ChildProcess>;
};

export = open;
export {apps};
export default open;
69 changes: 58 additions & 11 deletions index.js
@@ -1,11 +1,14 @@
const path = require('path');
const childProcess = require('child_process');
const {promises: fs, constants: fsConstants} = require('fs');
const isWsl = require('is-wsl');
const isDocker = require('is-docker');
const defineLazyProperty = require('define-lazy-prop');
import path from 'path';
import {fileURLToPath} from 'url';
import childProcess from 'child_process';
import {promises as fs, constants as fsConstants} from 'fs';
import isWsl from 'is-wsl';
import isDocker from 'is-docker';
import defineLazyProperty from 'define-lazy-prop';
import defaultBrowser from 'default-browser';

// Path to included `xdg-open`.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const localXdgOpenPath = path.join(__dirname, 'xdg-open');

const {platform, arch} = process;
Expand Down Expand Up @@ -117,6 +120,45 @@ const baseOpen = async options => {
}));
}

if (app === 'browser' || app === 'browserPrivate') {
// IDs from default-browser for macOS and windows are the same
const ids = {
'com.google.chrome': 'chrome',
'google-chrome.desktop': 'chrome',
'org.mozilla.firefox': 'firefox',
'firefox.desktop': 'firefox',
'com.microsoft.msedge': 'edge',
'com.microsoft.edge': 'edge',
'microsoft-edge.desktop': 'edge'
};

// Incognito flags for each browser in open.apps
const flags = {
chrome: '--incognito',
firefox: '--private-window',
edge: '--inPrivate'
};

const browser = await defaultBrowser();
if (browser.id in ids) {
const browserName = ids[browser.id];

if (app === 'browserPrivate') {
appArguments.push(flags[browserName]);
}

return baseOpen({
...options,
app: {
name: open.apps[browserName],
arguments: appArguments
}
});
}

throw new Error(`${browser.name} is not supported as a default browser`);
}

let command;
const cliArguments = [];
const childProcessOptions = {};
Expand Down Expand Up @@ -149,7 +191,7 @@ const baseOpen = async options => {
cliArguments.push(
'-NoProfile',
'-NonInteractive',
'ExecutionPolicy',
'-ExecutionPolicy',
'Bypass',
'-EncodedCommand'
);
Expand All @@ -167,17 +209,17 @@ const baseOpen = async options => {
if (app) {
// 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');
encodedArguments.push(`"\`"${app}\`""`);
if (options.target) {
appArguments.unshift(options.target);
appArguments.push(options.target);
}
} else if (options.target) {
encodedArguments.push(`"${options.target}"`);
}

if (appArguments.length > 0) {
appArguments = appArguments.map(arg => `"\`"${arg}\`""`);
encodedArguments.push(appArguments.join(','));
encodedArguments.push('-ArgumentList', appArguments.join(','));
}

// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
Expand Down Expand Up @@ -328,7 +370,12 @@ defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
}));

defineLazyProperty(apps, 'browser', () => 'browser');

defineLazyProperty(apps, 'browserPrivate', () => 'browserPrivate');

open.apps = apps;
open.openApp = openApp;

module.exports = open;
export {apps};
export default open;
2 changes: 1 addition & 1 deletion index.test-d.ts
@@ -1,6 +1,6 @@
import {expectType} from 'tsd';
import {ChildProcess} from 'child_process';
import open = require('.');
import open from '.';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const options: open.Options = {};
Expand Down
6 changes: 4 additions & 2 deletions package.json
@@ -1,6 +1,7 @@
{
"name": "open",
"version": "8.4.2",
"version": "8.4.0",
"type": "module",
"description": "Open stuff like URLs, files, executables. Cross-platform.",
"license": "MIT",
"repository": "sindresorhus/open",
Expand Down Expand Up @@ -50,7 +51,8 @@
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
"is-wsl": "^2.2.0",
"default-browser": "^3.1.0"
},
"devDependencies": {
"@types/node": "^15.0.0",
Expand Down
28 changes: 23 additions & 5 deletions readme.md
Expand Up @@ -26,7 +26,7 @@ npm install open
## Usage

```js
const open = require('open');
import open from 'open';

// Opens the image in the default image viewer and waits for the opened app to quit.
await open('unicorn.png', {wait: true});
Expand All @@ -41,10 +41,13 @@ 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
// Opens the URL in the default browser in incognito mode.
await open('https://sindresorhus.com', {app: {name: open.apps.browserPrivate}});

// Open an app.
await open.openApp('xcode');

// Open an app with arguments
// Open an app with arguments.
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});
```

Expand Down Expand Up @@ -116,25 +119,40 @@ Allow the opened app to exit with nonzero exit code when the `wait` option is `t

We do not recommend setting this option. The convention for success is exit code zero.

### open.apps
### open.apps / apps

An object containing auto-detected binary names for common apps. Useful to work around [cross-platform differences](#app).

```js
const open = require('open');
// Using default export.
import open from 'open';

await open('https://google.com', {
app: {
name: open.apps.chrome
}
});

// Using named export.
import open, {apps} from 'open';

await open('https://firefox.com', {
app: {
name: apps.browserPrivate
}
});
```
`browser` and `browserPrivate` can also be used to access the user's default browser through [`default-browser`](https://github.com/sindresorhus/default-browser).

#### Supported apps

- [`chrome`](https://www.google.com/chrome) - Web browser
- [`firefox`](https://www.mozilla.org/firefox) - Web browser
- [`edge`](https://www.microsoft.com/edge) - Web browser
- `browser` - Default web browser
- `browserPrivate` - Default web browser in incognito mode

`browser` and `browserPrivate` only supports `chrome`, `firefox` and `edge`.

### open.openApp(name, options?)

Expand Down
20 changes: 18 additions & 2 deletions test.js
@@ -1,5 +1,5 @@
const test = require('ava');
const open = require('.');
import test from 'ava';
import open from './index.js';
const {openApp} = open;

// Tests only checks that opening doesn't return an error
Expand Down Expand Up @@ -78,3 +78,19 @@ test('open Firefox without arguments', async t => {
test('open Chrome in incognito mode', async t => {
await t.notThrowsAsync(openApp(open.apps.chrome, {arguments: ['--incognito'], newInstance: true}));
});

test('open URL with default browser argument', async t => {
await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: open.apps.browser}}));
});

test('open URL with default browser in incognito mode', async t => {
await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: open.apps.browserPrivate}}));
});

test('open default browser', async t => {
await t.notThrowsAsync(openApp(open.apps.browser, {newInstance: true}));
});

test('open default browser in incognito mode', async t => {
await t.notThrowsAsync(openApp(open.apps.browserPrivate, {newInstance: true}));
});

0 comments on commit 3b79981

Please sign in to comment.