From 307988813ee81ac7a007f410a6003656bd1cea1f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 13 Feb 2021 14:05:32 +1300 Subject: [PATCH 01/17] Require Node.js 12, add `.apps` and allow multiple apps to be tried Signed-off-by: Richie Bendall --- index.d.ts | 69 +++++++++++++++------------ index.js | 123 ++++++++++++++++++++++++++++++++++++------------ index.test-d.ts | 12 +++-- package.json | 17 ++++--- readme.md | 50 ++++++++++---------- test.js | 99 ++++++++++++-------------------------- 6 files changed, 208 insertions(+), 162 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4365e1f..111144e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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. + + @example + ``` + import open, {apps} from 'open'; + + await open('https://google.com', { + app: { + name: apps.chrome + } + }); + ``` + */ + apps: Record; + + /** + 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'); @@ -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; - -export = open; + ``` + */ + ( + target: string, + options?: open.Options + ): Promise; +}; + +export default open; diff --git a/index.js b/index.js index 97f218a..76f05d7 100644 --- a/index.js +++ b/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) +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. @@ -31,9 +34,9 @@ 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 @@ -41,16 +44,30 @@ const getWslDrivesMountPoint = (() => { 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*(?.*)/g.exec(configContent)?.groups?.mountPoint || '').trim(); mountPoint = mountPoint.endsWith('/') ? mountPoint : mountPoint + '/'; return mountPoint; }; })(); -module.exports = async (target, options) => { +const pTryEach = async (array, mapper) => { + const errors = []; + + for await (const item of array) { + try { + return await mapper(item); + } catch (error) { + errors.push(error); + } + } + + throw new AggregateError(errors); +}; + +const open = async (target, options) => { if (typeof target !== 'string') { throw new TypeError('Expected a `target`'); } @@ -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) { @@ -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 ? @@ -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; } @@ -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); } @@ -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' +})); + +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; diff --git a/index.test-d.ts b/index.test-d.ts index 26f3fd2..55b301c 100644 --- a/index.test-d.ts +++ b/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>(open('foo')); -expectType>(open('foo', {app: 'bar'})); -expectType>(open('foo', {app: ['bar', '--arg']})); +expectType>(open('foo', {app: { + name: 'bar' +}})); +expectType>(open('foo', {app: { + name: 'bar', + arguments: ['--arg'] +}})); expectType>(open('foo', {wait: true})); expectType>(open('foo', {background: true})); -expectType>(open('foo', {url: true})); diff --git a/package.json b/package.json index 778e732..9b28fad 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, + "type": "module", "engines": { - "node": ">=8" + "node": ">=12" }, "scripts": { "test": "xo && tsd" @@ -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", + "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" } } diff --git a/readme.md b/readme.md index c4560a7..badd214 100644 --- a/readme.md +++ b/readme.md @@ -26,22 +26,20 @@ $ npm install open ## Usage ```js -const open = require('open'); +import open from 'open'; -(async () => { - // Opens the image in the default image viewer and waits for the opened app to quit. - await open('unicorn.png', {wait: true}); - console.log('The image viewer app quit'); +// Opens the image in the default image viewer and waits for the opened app to quit. +await open('unicorn.png', {wait: true}); +console.log('The image viewer app quit'); - // Opens the URL in the default browser. - await open('https://sindresorhus.com'); +// 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: 'firefox'}); +// Opens the URL in a specified browser. +await open('https://sindresorhus.com', {app: 'firefox'}); - // Specify app arguments. - await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']}); -})(); +// Specify app arguments. +await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']}); ``` ## API @@ -84,32 +82,36 @@ Do not bring the app to the foreground. ##### app -Type: `string | string[]` +Type: `{name: string | string[], arguments?: string[]} | Array<{name: string | string[], arguments: string[]}>` -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. -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. -##### url +##### allowNonzeroExitCode Type: `boolean`\ Default: `false` -Uses `URL` to encode the target before executing it.
-We do not recommend using it on targets that are not URLs. +Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. -Especially useful when dealing with the [double-quotes on Windows](#double-quotes-on-windows) caveat. +We do not recommend setting this option. The convention for success is exit code zero. -##### allowNonzeroExitCode +### open.apps -Type: `boolean`\ -Default: `false` +An object containing auto-detected binary names for common apps. Useful to work around [cross-platform issues](#app). -Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. +```js +import open, {apps} from 'open'; -We do not recommend setting this option. The convention for success is exit code zero. +await open('https://google.com', { + app: { + name: apps.chrome + } +}); +``` ## Caveats diff --git a/test.js b/test.js index 1caf538..d71fffe 100644 --- a/test.js +++ b/test.js @@ -1,105 +1,68 @@ import test from 'ava'; -import isWsl from 'is-wsl'; -import open from '.'; - -let chromeName; -let firefoxName; -let chromeWslName; -let firefoxWslName; - -if (process.platform === 'darwin') { - chromeName = 'google chrome canary'; - firefoxName = 'firefox'; -} else if (process.platform === 'win32' || isWsl) { - chromeName = 'Chrome'; - firefoxName = 'C:\\Program Files\\Mozilla Firefox\\firefox.exe'; - chromeWslName = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'; - firefoxWslName = '/mnt/c/Program Files/Mozilla Firefox/firefox.exe'; -} else if (process.platform === 'linux') { - chromeName = 'google-chrome'; - firefoxName = 'firefox'; -} +import open from './index.js'; // Tests only checks that opening doesn't return an error // it has no way make sure that it actually opened anything. // These have to be manually verified. -test('open file in default app', async () => { - await open('index.js'); +test('open file in default app', async t => { + await t.notThrowsAsync(() => open('index.js')); }); -test('wait for the app to close if wait: true', async () => { - await open('https://sindresorhus.com', {wait: true}); +test('wait for the app to close if wait: true', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com', {wait: true})); }); -test('encode URL if url: true', async () => { - await open('https://sindresorhus.com', {url: true}); +test('encode URL if url: true', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com', {url: true})); }); -test('open URL in default app', async () => { - await open('https://sindresorhus.com'); +test('open URL in default app', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com')); }); -test('open URL in specified app', async () => { - await open('https://sindresorhus.com', {app: firefoxName}); +test('open URL in specified app', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com', {app: {name: open.apps.chrome}})); }); -test('open URL in specified app with arguments', async () => { - await open('https://sindresorhus.com', {app: [chromeName, '--incognito']}); +test('open URL in specified app with arguments', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com', {app: {name: open.apps.chrome, arguments: ['--incognito']}})); }); test('return the child process when called', async t => { - const cp = await open('index.js'); - t.true('stdout' in cp); + const childProcess = await open('index.js'); + t.true('stdout' in childProcess); }); -test('open URL with query strings', async () => { - await open('https://sindresorhus.com/?abc=123&def=456'); +test('open URL with query strings', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456')); }); -test('open URL with a fragment', async () => { - await open('https://sindresorhus.com#projects'); +test('open URL with a fragment', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com#projects')); }); -test('open URL with query strings and spaces', async () => { - await open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces'); +test('open URL with query strings and spaces', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces')); }); -test('open URL with query strings and a fragment', async () => { - await open('https://sindresorhus.com/?abc=123&def=456#projects'); +test('open URL with query strings and a fragment', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456#projects')); }); -test('open URL with query strings and pipes', async () => { - await open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h'); +test('open URL with query strings and pipes', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h')); }); -test('open URL with query strings, spaces, pipes and a fragment', async () => { - await open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects'); +test('open URL with query strings, spaces, pipes and a fragment', async t => { + await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects')); }); -test('open URL with query strings and URL reserved characters', async () => { - await 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'); +test('open URL with query strings and URL reserved characters', 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')); }); -test('open URL with query strings and URL reserved characters with `url` option', async () => { - await 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 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})); }); - -if (isWsl) { - test('open URL in specified Windows app given a WSL path to the app', async () => { - await open('https://sindresorhus.com', {app: firefoxWslName}); - }); - - test('open URL in specified Windows app with arguments given a WSL path to the app', async () => { - await open('https://sindresorhus.com', {app: [chromeWslName, '--incognito']}); - }); - - test('open URL with query strings and spaces works with `url` option', async () => { - await open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces', {url: true}); - }); - - test('open URL with query strings works with `url` option', async () => { - await open('https://sindresorhus.com/?abc=123&def=456', {url: true}); - }); -} From 5d27433456166f94bbf69ebadcf9e1417171a215 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 13 Feb 2021 14:09:01 +1300 Subject: [PATCH 02/17] Update main.yml --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18531b3..41fe626 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,6 @@ jobs: node-version: - 14 - 12 - - 10 - - 8 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 From 0f096f68c5e53bfb3746bbddea7434a504c226b5 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 13 Feb 2021 16:42:28 +1300 Subject: [PATCH 03/17] Update index.d.ts --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 111144e..fb70234 100644 --- a/index.d.ts +++ b/index.d.ts @@ -60,7 +60,7 @@ declare const open: { }); ``` */ - apps: Record; + apps: Record; /** Open stuff like URLs, files, executables. Cross-platform. From 92b842b69dc998cc45758d81489af6846d64f173 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 17 Feb 2021 15:01:03 +1300 Subject: [PATCH 04/17] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 1b77ac9..53347cb 100644 --- a/index.js +++ b/index.js @@ -46,7 +46,7 @@ const getWslDrivesMountPoint = (() => { return defaultMountPoint; } - const configContent = await pReadFile(configFilePath, {encoding: 'utf8'}); + const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'}); const configMountPoint = /root\s*=\s*(?.*)/g.exec(configContent)?.groups?.mountPoint?.trim(); if (!configMountPoint) { From aacf288b657dc45e7380ee6057f6738ca70dc9b7 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 17 Feb 2021 23:08:07 +1300 Subject: [PATCH 05/17] Minor tweaks Signed-off-by: Richie Bendall --- index.js | 10 ++++------ test.js | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 53347cb..0142854 100644 --- a/index.js +++ b/index.js @@ -26,8 +26,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 @@ -47,22 +45,22 @@ const getWslDrivesMountPoint = (() => { } const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'}); - const configMountPoint = /root\s*=\s*(?.*)/g.exec(configContent)?.groups?.mountPoint?.trim(); + const configMountPoint = /root\s*=\s*(?.*)/g.exec(configContent); if (!configMountPoint) { return defaultMountPoint; } - mountPoint = configMountPoint.endsWith('/') ? configMountPoint : `${configMountPoint}/`; + const mountPoint = configMountPoint.groups.mountPoint.trim() - return mountPoint; + return mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`; }; })(); const pTryEach = async (array, mapper) => { const errors = []; - for await (const item of array) { + for (const item of array) { try { return await mapper(item); } catch (error) { diff --git a/test.js b/test.js index d71fffe..ff54abd 100644 --- a/test.js +++ b/test.js @@ -7,27 +7,27 @@ import open from './index.js'; // These have to be manually verified. test('open file in default app', async t => { - await t.notThrowsAsync(() => open('index.js')); + await t.notThrowsAsync(open('index.js')); }); test('wait for the app to close if wait: true', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com', {wait: true})); + await t.notThrowsAsync(open('https://sindresorhus.com', {wait: true})); }); test('encode URL if url: true', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com', {url: true})); + await t.notThrowsAsync(open('https://sindresorhus.com', {url: true})); }); test('open URL in default app', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com')); + await t.notThrowsAsync(open('https://sindresorhus.com')); }); test('open URL in specified app', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com', {app: {name: open.apps.chrome}})); + await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: open.apps.chrome}})); }); test('open URL in specified app with arguments', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com', {app: {name: open.apps.chrome, arguments: ['--incognito']}})); + await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: open.apps.chrome, arguments: ['--incognito']}})); }); test('return the child process when called', async t => { @@ -36,33 +36,33 @@ test('return the child process when called', async t => { }); test('open URL with query strings', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456')); + await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456')); }); test('open URL with a fragment', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com#projects')); + await t.notThrowsAsync(open('https://sindresorhus.com#projects')); }); test('open URL with query strings and spaces', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces')); + await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces')); }); test('open URL with query strings and a fragment', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456#projects')); + await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456#projects')); }); test('open URL with query strings and pipes', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h')); + await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h')); }); test('open URL with query strings, spaces, pipes and a fragment', async t => { - await t.notThrowsAsync(() => open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects')); + await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects')); }); test('open URL with query strings and URL reserved characters', 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')); + 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')); }); 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})); + 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})); }); From f7e68837db552ff6cef2779c2a6d3662c45cc84b Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 17 Feb 2021 23:19:36 +1300 Subject: [PATCH 06/17] Require Node.js 10 instead Signed-off-by: Richie Bendall --- .github/workflows/main.yml | 1 + index.d.ts | 8 ++++---- index.js | 26 +++++++++++--------------- index.test-d.ts | 2 +- package.json | 3 +-- readme.md | 6 +++--- test.js | 4 ++-- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41fe626..c1870cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: node-version: - 14 - 12 + - 10 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 diff --git a/index.d.ts b/index.d.ts index fb70234..98c5dd4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -51,11 +51,11 @@ declare const open: { @example ``` - import open, {apps} from 'open'; + import open = require('open'); await open('https://google.com', { app: { - name: apps.chrome + name: open.apps.chrome } }); ``` @@ -74,7 +74,7 @@ declare const open: { @example ``` - import open from 'open'; + import open = require('open'); // Opens the image in the default image viewer await open('unicorn.png', {wait: true}); @@ -96,4 +96,4 @@ declare const open: { ): Promise; }; -export default open; +export = open; diff --git a/index.js b/index.js index 0142854..ce1351d 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,13 @@ -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) -const currentDirectoryName = path.dirname(fileURLToPath(import.meta.url)); +const path = require('path'); +const childProcess = require('child_process'); +const {promises: fs} = require('fs'); +const isWsl = require('is-wsl'); +const isDocker = require('is-docker'); +const defineLazyProperty = require('define-lazy-prop'); +const AggregateError = require('aggregate-error'); // Path to included `xdg-open`. -const localXdgOpenPath = path.join(currentDirectoryName, 'xdg-open'); +const localXdgOpenPath = path.join(__dirname, 'xdg-open'); const {platform} = process; @@ -51,7 +47,7 @@ const getWslDrivesMountPoint = (() => { return defaultMountPoint; } - const mountPoint = configMountPoint.groups.mountPoint.trim() + const mountPoint = configMountPoint.groups.mountPoint.trim(); return mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`; }; @@ -62,7 +58,7 @@ const pTryEach = async (array, mapper) => { for (const item of array) { try { - return await mapper(item); + return await mapper(item); // eslint-disable-line no-await-in-loop } catch (error) { errors.push(error); } @@ -166,7 +162,7 @@ const open = async (target, options) => { command = app; } else { // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. - const isBundled = !currentDirectoryName || currentDirectoryName === '/'; + const isBundled = !__dirname || __dirname === '/'; // Check if local `xdg-open` exists and is executable. let exeLocalXdgOpen = false; diff --git a/index.test-d.ts b/index.test-d.ts index 55b301c..9028cb9 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,6 +1,6 @@ import {expectType} from 'tsd'; import {ChildProcess} from 'child_process'; -import open from './index.js'; +import open = require('.'); const options: open.Options = {}; diff --git a/package.json b/package.json index ce7f5dc..375c22b 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, - "type": "module", "engines": { - "node": ">=12" + "node": ">=10" }, "scripts": { "test": "xo && tsd" diff --git a/readme.md b/readme.md index badd214..0af54cb 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,7 @@ $ npm install open ## Usage ```js -import open from 'open'; +const open = require('open'); // Opens the image in the default image viewer and waits for the opened app to quit. await open('unicorn.png', {wait: true}); @@ -104,11 +104,11 @@ We do not recommend setting this option. The convention for success is exit code An object containing auto-detected binary names for common apps. Useful to work around [cross-platform issues](#app). ```js -import open, {apps} from 'open'; +const open = require('open'); await open('https://google.com', { app: { - name: apps.chrome + name: open.apps.chrome } }); ``` diff --git a/test.js b/test.js index ff54abd..271043f 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ -import test from 'ava'; -import open from './index.js'; +const test = require('ava'); +const open = require('.'); // Tests only checks that opening doesn't return an error // it has no way make sure that it actually opened anything. From d239284cd57046590cc28330c804af722e3bbcf0 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 22 Feb 2021 08:35:21 +1300 Subject: [PATCH 07/17] Only throw the last error Signed-off-by: Richie Bendall --- index.d.ts | 6 +++--- index.js | 7 +++---- package.json | 1 - readme.md | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 98c5dd4..d53f2f4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,11 +24,11 @@ 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. + 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?: App | readonly App[]; diff --git a/index.js b/index.js index ce1351d..2b94861 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,6 @@ const {promises: fs} = require('fs'); const isWsl = require('is-wsl'); const isDocker = require('is-docker'); const defineLazyProperty = require('define-lazy-prop'); -const AggregateError = require('aggregate-error'); // Path to included `xdg-open`. const localXdgOpenPath = path.join(__dirname, 'xdg-open'); @@ -54,17 +53,17 @@ const getWslDrivesMountPoint = (() => { })(); const pTryEach = async (array, mapper) => { - const errors = []; + let latestError for (const item of array) { try { return await mapper(item); // eslint-disable-line no-await-in-loop } catch (error) { - errors.push(error); + latestError = error } } - throw new AggregateError(errors); + throw latestError }; const open = async (target, options) => { diff --git a/package.json b/package.json index 375c22b..8980c6b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "file" ], "dependencies": { - "aggregate-error": "^3.1.0", "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" diff --git a/readme.md b/readme.md index 0af54cb..09f31fd 100644 --- a/readme.md +++ b/readme.md @@ -84,7 +84,7 @@ Do not bring the app to the foreground. 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. +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. From b0e27fba15ef1a55852ece6a291fadb2fb1616ba Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 22 Feb 2021 12:38:42 +1300 Subject: [PATCH 08/17] Fix lint Signed-off-by: Richie Bendall --- index.js | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 2b94861..38cbd24 100644 --- a/index.js +++ b/index.js @@ -53,17 +53,17 @@ const getWslDrivesMountPoint = (() => { })(); const pTryEach = async (array, mapper) => { - let latestError + let latestError; for (const item of array) { try { return await mapper(item); // eslint-disable-line no-await-in-loop } catch (error) { - latestError = error + latestError = error; } } - throw latestError + throw latestError; }; const open = async (target, options) => { diff --git a/package.json b/package.json index 8980c6b..2a1b909 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "https://sindresorhus.com" }, "engines": { - "node": ">=10" + "node": "^10.17 || >=11.14" }, "scripts": { "test": "xo && tsd" From ceac9b7031a0a30a0fa42cf4a23d1b7e8d46c8bd Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:07:54 +1300 Subject: [PATCH 09/17] Update package.json Co-authored-by: Sindre Sorhus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a1b909..3a463c5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "https://sindresorhus.com" }, "engines": { - "node": "^10.17 || >=11.14" + "node": "^10.17" }, "scripts": { "test": "xo && tsd" From ba4b2c9f8e1594175fc3751ba84f34e60b1e7c87 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:08:42 +1300 Subject: [PATCH 10/17] Update index.d.ts --- index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index d53f2f4..341bdc5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,11 +24,11 @@ declare namespace open { readonly background?: 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. + 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. + 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?: App | readonly App[]; @@ -47,7 +47,7 @@ declare namespace open { declare const open: { /** - An object containing auto-detected binary names for common apps. Useful to work around cross-platform issues. + An object containing auto-detected binary names for common apps. Useful to work around cross-platform differences. @example ``` From 233ed5dc8fe76d0b60268ab967398c44e3215c2f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:09:38 +1300 Subject: [PATCH 11/17] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 38cbd24..74c08a3 100644 --- a/index.js +++ b/index.js @@ -247,4 +247,4 @@ defineLazyProperty(apps, 'firefox', () => detectPlatformBinary(new Map([ open.apps = apps; -export default open; +module.exports = open; From 400e62b4aa27ca61beb7699929dde577a683d43a Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:11:34 +1300 Subject: [PATCH 12/17] Update readme.md --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 09f31fd..c905c47 100644 --- a/readme.md +++ b/readme.md @@ -36,10 +36,10 @@ console.log('The image viewer app quit'); await open('https://sindresorhus.com'); // Opens the URL in a specified browser. -await open('https://sindresorhus.com', {app: 'firefox'}); +await open('https://sindresorhus.com', {app: {name: 'firefox'}}); // Specify app arguments. -await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']}); +await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: '--incognito'}}); ``` ## API @@ -101,7 +101,7 @@ We do not recommend setting this option. The convention for success is exit code ### open.apps -An object containing auto-detected binary names for common apps. Useful to work around [cross-platform issues](#app). +An object containing auto-detected binary names for common apps. Useful to work around [cross-platform differences](#app). ```js const open = require('open'); From 4905777bd4f466030678164c03ba5c16e5a7dbf9 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:12:53 +1300 Subject: [PATCH 13/17] Update index.d.ts --- index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 341bdc5..71910e1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -83,11 +83,11 @@ declare const open: { // 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'}); + // Opens the URL in a specified browser. + await open('https://sindresorhus.com', {app: {name: 'firefox'}}); - // Specify app arguments - await open('https://sindresorhus.com', {app: ['google chrome', '--incognito']}); + // Specify app arguments. + await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: '--incognito'}}); ``` */ ( From 53098ef434f00cb7ce35006462db278e59ffd536 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:16:50 +1300 Subject: [PATCH 14/17] Update index.d.ts --- index.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 71910e1..5ef18af 100644 --- a/index.d.ts +++ b/index.d.ts @@ -42,7 +42,11 @@ declare namespace open { readonly allowNonzeroExitCode?: boolean; } - type App = {name: string | readonly string[]; arguments?: readonly string[]}; + type AppName = + | 'chrome' + | 'firefox'; + + type App = {name: AppName | readonly AppName[]; arguments?: readonly string[]}; } declare const open: { From f2d5adb54ece80e060bb5096f63022eb06e2d01f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:17:33 +1300 Subject: [PATCH 15/17] Update index.d.ts --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5ef18af..74288bf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -46,7 +46,7 @@ declare namespace open { | 'chrome' | 'firefox'; - type App = {name: AppName | readonly AppName[]; arguments?: readonly string[]}; + type App = {name: string | readonly string[]; arguments?: readonly string[]}; } declare const open: { @@ -64,7 +64,7 @@ declare const open: { }); ``` */ - apps: Record; + apps: Record; /** Open stuff like URLs, files, executables. Cross-platform. From b022228c284a7cce983b3a4c063ca2833b6a4320 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:17:48 +1300 Subject: [PATCH 16/17] Update index.d.ts --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 74288bf..667bdcc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -64,7 +64,7 @@ declare const open: { }); ``` */ - apps: Record; + apps: Record; /** Open stuff like URLs, files, executables. Cross-platform. From d63d40ce9f2bfbf492515bf27668677a64944993 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 27 Feb 2021 14:23:06 +1300 Subject: [PATCH 17/17] Update readme.md --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index c905c47..81eb060 100644 --- a/readme.md +++ b/readme.md @@ -113,6 +113,11 @@ await open('https://google.com', { }); ``` +#### Supported apps + +- [`chrome`](https://www.google.com/chrome) - Web browser +- [`firefox`](https://www.mozilla.org/firefox) - Web browser + ## Caveats ### Double-quotes on Windows