Skip to content

Commit

Permalink
Require Node.js 10.17, add .apps and allow multiple apps to be tried (
Browse files Browse the repository at this point in the history
#222)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Richienb and sindresorhus committed Feb 28, 2021
1 parent a9babe0 commit 8c00bd8
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 166 deletions.
1 change: 0 additions & 1 deletion .github/workflows/main.yml
Expand Up @@ -13,7 +13,6 @@ jobs:
- 14
- 12
- 10
- 8
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
Expand Down
83 changes: 49 additions & 34 deletions index.d.ts
Expand Up @@ -24,65 +24,80 @@ declare namespace open {
readonly background?: boolean;

/**
Specify the app to open the `target` with, or an array with the app and app arguments.
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.
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.
*/
readonly app?: string | readonly string[];

/**
__deprecated__
This option will be removed in the next major release.
*/
readonly url?: boolean;
readonly app?: App | readonly App[];

/**
Allow the opened app to exit with nonzero exit code when the `wait` option is `true`.
We do not recommend setting this option. The convention for success is exit code zero.
@default false
*/
readonly allowNonzeroExitCode?: boolean;
}

type AppName =
| 'chrome'
| 'firefox';

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

/**
Open stuff like URLs, files, executables. Cross-platform.
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 | string[]>;

/**
Open stuff like URLs, files, executables. Cross-platform.
Uses the command `open` on OS X, `start` on Windows and `xdg-open` on other platforms.
Uses the command `open` on OS X, `start` on Windows and `xdg-open` on other platforms.
There is a caveat for [double-quotes on Windows](https://github.com/sindresorhus/open#double-quotes-on-windows) where all double-quotes are stripped from the `target`.
There is a caveat for [double-quotes on Windows](https://github.com/sindresorhus/open#double-quotes-on-windows) where all double-quotes are stripped from the `target`.
@param target - The thing you want to open. Can be a URL, file, or executable. Opens in the default app for the file type. For example, URLs open in your default browser.
@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.
@param target - The thing you want to open. Can be a URL, file, or executable. Opens in the default app for the file type. For example, URLs open in your default browser.
@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
```
import open = require('open');
@example
```
import open = require('open');
// Opens the image in the default image viewer
(async () => {
// Opens the image in the default image viewer
await open('unicorn.png', {wait: true});
console.log('The image viewer app closed');
// Opens the url in the default browser
await open('https://sindresorhus.com');
// Specify the app to open in
await open('https://sindresorhus.com', {app: 'firefox'});
// Specify app arguments
await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']});
})();
```
*/
declare function open(
target: string,
options?: open.Options
): Promise<ChildProcess>;
// 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'}});
```
*/
(
target: string,
options?: open.Options
): Promise<ChildProcess>;
};

export = open;
113 changes: 84 additions & 29 deletions index.js
@@ -1,17 +1,15 @@
'use strict';
const {promisify} = require('util');
const path = require('path');
const childProcess = require('child_process');
const fs = require('fs');
const {promises: fs} = require('fs');
const isWsl = require('is-wsl');
const isDocker = require('is-docker');

const pAccess = promisify(fs.access);
const pReadFile = promisify(fs.readFile);
const defineLazyProperty = require('define-lazy-prop');

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

const {platform} = process;

/**
Get the mount point for fixed drives in WSL.
Expand All @@ -23,8 +21,6 @@ const getWslDrivesMountPoint = (() => {
// according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
const defaultMountPoint = '/mnt/';

let mountPoint;

return async function () {
if (mountPoint) {
// Return memoized mount point value
Expand All @@ -35,29 +31,42 @@ const getWslDrivesMountPoint = (() => {

let isConfigFileExists = false;
try {
await pAccess(configFilePath, fs.constants.F_OK);
await fs.access(configFilePath, fs.constants.F_OK);
isConfigFileExists = true;
} catch (_) {}
} catch {}

if (!isConfigFileExists) {
return defaultMountPoint;
}

const configContent = await pReadFile(configFilePath, {encoding: 'utf8'});
const configMountPoint = /root\s*=\s*(.*)/g.exec(configContent);
const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'});
const configMountPoint = /root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);

if (!configMountPoint) {
return defaultMountPoint;
}

mountPoint = configMountPoint[1].trim();
mountPoint = mountPoint.endsWith('/') ? mountPoint : mountPoint + '/';
const mountPoint = configMountPoint.groups.mountPoint.trim();

return mountPoint;
return mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`;
};
})();

module.exports = async (target, options) => {
const pTryEach = async (array, mapper) => {
let latestError;

for (const item of array) {
try {
return await mapper(item); // eslint-disable-line no-await-in-loop
} catch (error) {
latestError = error;
}
}

throw latestError;
};

const open = async (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}
Expand All @@ -69,18 +78,30 @@ module.exports = async (target, options) => {
...options
};

let command;
let {app} = options;
let appArguments = [];
const cliArguments = [];
const childProcessOptions = {};
if (Array.isArray(options.app)) {
return pTryEach(options.app, singleApp => open(target, {
...options,
app: singleApp
}));
}

let {name: app, appArguments = []} = options.app ?? {};

if (Array.isArray(app)) {
appArguments = app.slice(1);
app = app[0];
return pTryEach(app, appName => open(target, {
...options,
app: {
name: appName,
arguments: appArguments
}
}));
}

if (process.platform === 'darwin') {
let command;
const cliArguments = [];
const childProcessOptions = {};

if (platform === 'darwin') {
command = 'open';

if (options.wait) {
Expand All @@ -94,7 +115,7 @@ module.exports = async (target, options) => {
if (app) {
cliArguments.push('-a', app);
}
} else if (process.platform === 'win32' || (isWsl && !isDocker())) {
} else if (platform === 'win32' || (isWsl && !isDocker())) {
const mountPoint = await getWslDrivesMountPoint();

command = isWsl ?
Expand Down Expand Up @@ -145,12 +166,12 @@ module.exports = async (target, options) => {
// Check if local `xdg-open` exists and is executable.
let exeLocalXdgOpen = false;
try {
await pAccess(localXdgOpenPath, fs.constants.X_OK);
await fs.access(localXdgOpenPath, fs.constants.X_OK);
exeLocalXdgOpen = true;
} catch (_) {}
} catch {}

const useSystemXdgOpen = process.versions.electron ||
process.platform === 'android' || isBundled || !exeLocalXdgOpen;
platform === 'android' || isBundled || !exeLocalXdgOpen;
command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
}

Expand All @@ -168,7 +189,7 @@ module.exports = async (target, options) => {

cliArguments.push(target);

if (process.platform === 'darwin' && appArguments.length > 0) {
if (platform === 'darwin' && appArguments.length > 0) {
cliArguments.push('--args', ...appArguments);
}

Expand All @@ -193,3 +214,37 @@ module.exports = async (target, options) => {

return subprocess;
};

function detectPlatformBinary(platformMap, {wsl}) {
if (wsl && isWsl) {
return wsl;
}

if (!platformMap.has(platform)) {
throw new Error(`${platform} is not supported`);
}

return platformMap.get(platform);
}

const apps = {};

defineLazyProperty(apps, 'chrome', () => detectPlatformBinary(new Map([
['darwin', 'google chrome canary'],
['win32', 'Chrome'],
['linux', ['google-chrome', 'google-chrome-stable']]
]), {
wsl: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'
}));

defineLazyProperty(apps, 'firefox', () => detectPlatformBinary(new Map([
['darwin', 'firefox'],
['win32', 'C:\\Program Files\\Mozilla Firefox\\firefox.exe'],
['linux', 'firefox']
]), {
wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe'
}));

open.apps = apps;

module.exports = open;
10 changes: 7 additions & 3 deletions index.test-d.ts
Expand Up @@ -5,8 +5,12 @@ import open = require('.');
const options: open.Options = {};

expectType<Promise<ChildProcess>>(open('foo'));
expectType<Promise<ChildProcess>>(open('foo', {app: 'bar'}));
expectType<Promise<ChildProcess>>(open('foo', {app: ['bar', '--arg']}));
expectType<Promise<ChildProcess>>(open('foo', {app: {
name: 'bar'
}}));
expectType<Promise<ChildProcess>>(open('foo', {app: {
name: 'bar',
arguments: ['--arg']
}}));
expectType<Promise<ChildProcess>>(open('foo', {wait: true}));
expectType<Promise<ChildProcess>>(open('foo', {background: true}));
expectType<Promise<ChildProcess>>(open('foo', {url: true}));
15 changes: 8 additions & 7 deletions package.json
Expand Up @@ -11,7 +11,7 @@
"url": "https://sindresorhus.com"
},
"engines": {
"node": ">=8"
"node": "^10.17"
},
"scripts": {
"test": "xo && tsd"
Expand Down Expand Up @@ -48,13 +48,14 @@
"file"
],
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"devDependencies": {
"@types/node": "^12.7.5",
"ava": "^2.3.0",
"tsd": "^0.11.0",
"xo": "^0.25.3"
"@types/node": "^14.14.27",
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.37.1"
}
}

0 comments on commit 8c00bd8

Please sign in to comment.