Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require Node.js 10.17, add .apps and allow multiple apps to be tried #222

Merged
merged 18 commits into from Feb 28, 2021
Merged
69 changes: 40 additions & 29 deletions index.d.ts
Expand Up @@ -30,42 +30,53 @@ declare namespace open {

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 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 issues.
Richienb marked this conversation as resolved.
Show resolved Hide resolved

@example
```
import open, {apps} from 'open';

await open('https://google.com', {
app: {
name: apps.chrome
}
});
```
*/
apps: Record<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 from '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');

Expand All @@ -77,12 +88,12 @@ import open = require('open');

// Specify app arguments
await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']});
})();
```
*/
declare function open(
target: string,
options?: open.Options
): Promise<ChildProcess>;

export = open;
```
*/
(
target: string,
options?: open.Options
): Promise<ChildProcess>;
};

export default open;
123 changes: 93 additions & 30 deletions index.js
@@ -1,16 +1,19 @@
'use strict';
const {promisify} = require('util');
const path = require('path');
const childProcess = require('child_process');
const fs = require('fs');
const isWsl = require('is-wsl');
const isDocker = require('is-docker');

const pAccess = promisify(fs.access);
const pReadFile = promisify(fs.readFile);
import path from 'path';
import childProcess from 'child_process';
import {promises as fs} from 'fs';
import {fileURLToPath} from 'url';
import isWsl from 'is-wsl';
import isDocker from 'is-docker';
import defineLazyProperty from 'define-lazy-prop';
import AggregateError from 'aggregate-error';

// Node.js ESM doesn't expose __dirname (https://stackoverflow.com/a/50052194/8384910)
Richienb marked this conversation as resolved.
Show resolved Hide resolved
const currentDirectoryName = path.dirname(fileURLToPath(import.meta.url));

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

const {platform} = process;

/**
Get the mount point for fixed drives in WSL.
Expand All @@ -31,26 +34,40 @@ 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) {
// Default value for "root" param
// according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
return '/mnt/';
}

const configContent = await pReadFile(configFilePath, {encoding: 'utf8'});
const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'});

mountPoint = (/root\s*=\s*(.*)/g.exec(configContent)[1] || '').trim();
mountPoint = (/root\s*=\s*(?<mountPoint>.*)/g.exec(configContent)?.groups?.mountPoint || '').trim();
mountPoint = mountPoint.endsWith('/') ? mountPoint : mountPoint + '/';

return mountPoint;
};
})();

module.exports = async (target, options) => {
const pTryEach = async (array, mapper) => {
Richienb marked this conversation as resolved.
Show resolved Hide resolved
const errors = [];

for await (const item of array) {
try {
return await mapper(item);
Richienb marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
errors.push(error);
}
}
Richienb marked this conversation as resolved.
Show resolved Hide resolved

throw new AggregateError(errors);
};

const open = async (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}
Expand All @@ -62,18 +79,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 @@ -87,7 +116,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 @@ -133,17 +162,17 @@ module.exports = async (target, options) => {
command = app;
} else {
// When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
const isBundled = !__dirname || __dirname === '/';
const isBundled = !currentDirectoryName || currentDirectoryName === '/';

// 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 @@ -161,7 +190,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 @@ -186,3 +215,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'
Richienb marked this conversation as resolved.
Show resolved Hide resolved
}));

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;

export default open;
Richienb marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 8 additions & 4 deletions index.test-d.ts
@@ -1,12 +1,16 @@
import {expectType} from 'tsd';
import {ChildProcess} from 'child_process';
import open = require('.');
import open from './index.js';

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}));
17 changes: 10 additions & 7 deletions package.json
Expand Up @@ -10,8 +10,9 @@
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
Richienb marked this conversation as resolved.
Show resolved Hide resolved
"engines": {
"node": ">=8"
"node": ">=12"
},
"scripts": {
"test": "xo && tsd"
Expand Down Expand Up @@ -48,13 +49,15 @@
"file"
],
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
"aggregate-error": "^3.1.0",
"define-lazy-prop": "^2.0.0",
Richienb marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}