From 3b7998193493ca5bb711b9a7b8bb65d5818afbb4 Mon Sep 17 00:00:00 2001 From: Leslie Yip Date: Mon, 20 Mar 2023 15:58:19 +0800 Subject: [PATCH] Add the ability to open default browser and default browser in private mode (#294) Co-authored-by: Sindre Sorhus --- index.d.ts | 62 ++++++++++++++++++++++++++++---------------- index.js | 69 +++++++++++++++++++++++++++++++++++++++++-------- index.test-d.ts | 2 +- package.json | 6 +++-- readme.md | 28 ++++++++++++++++---- test.js | 20 ++++++++++++-- 6 files changed, 144 insertions(+), 43 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9c8f2a3..f82d502 100644 --- a/index.d.ts +++ b/index.d.ts @@ -66,7 +66,9 @@ declare namespace open { type AppName = | 'chrome' | 'firefox' - | 'edge'; + | 'edge' + | 'browser' + | 'browserPrivate'; type App = { name: string | readonly string[]; @@ -74,6 +76,22 @@ declare namespace open { }; } +/** +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; + // eslint-disable-next-line no-redeclare declare const open: { /** @@ -88,13 +106,13 @@ 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. @@ -102,6 +120,9 @@ declare const open: { // 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}}); ``` */ ( @@ -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; + apps: typeof apps; /** Open an app. Cross-platform. @@ -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; }; -export = open; +export {apps}; +export default open; diff --git a/index.js b/index.js index f66dc4c..aef1558 100644 --- a/index.js +++ b/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; @@ -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 = {}; @@ -149,7 +191,7 @@ const baseOpen = async options => { cliArguments.push( '-NoProfile', '-NonInteractive', - '–ExecutionPolicy', + '-ExecutionPolicy', 'Bypass', '-EncodedCommand' ); @@ -167,9 +209,9 @@ 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}"`); @@ -177,7 +219,7 @@ const baseOpen = async options => { 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. @@ -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; diff --git a/index.test-d.ts b/index.test-d.ts index d57e60d..fb1649a 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 = require('.'); +import open from '.'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const options: open.Options = {}; diff --git a/package.json b/package.json index 8987b5a..38b864d 100644 --- a/package.json +++ b/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", @@ -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", diff --git a/readme.md b/readme.md index 282e0a6..aa9cbd8 100644 --- a/readme.md +++ b/readme.md @@ -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}); @@ -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']}); ``` @@ -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?) diff --git a/test.js b/test.js index 2fe1c38..af37e70 100644 --- a/test.js +++ b/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 @@ -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})); +});