From 5ecf3016c8252a35f7b84fe7e63ca1f97e3ce6d7 Mon Sep 17 00:00:00 2001 From: Marc Bernard <59966492+mbtools@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:41:50 -0400 Subject: [PATCH] fix: open URL in browser on WSL (#128) Here's a more elegant solution for opening URLs from WSL in your favorite browser. It is based on `sensible-browser` which is included in the default distribution for WSL. This avoids issues with the WSL environment, `cmd.exe`. quoting parameters, etc. In WSL, set the default browser using the `BROWSER` variable, for example, ```sh export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" or export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" ``` Note: To permanently set the default browser, add the appropriate entry to your shell's RC file, e.g. .bashrc or .zshrc. To launch a URL from the WSL command line: ```sh sensible-browser https://google.com ``` To launch a URL using `promise-spawn`: ```js const promiseSpawn = require('@npmcli/promise-spawn') promiseSpawn.open('https://google.com') ``` Replaces #118 Closes #62 ### Test ``` os: 5.15.153.1-microsoft-standard-WSL2 node: 20.18.0 npm: 10.8.2 ``` ![image](https://github.com/user-attachments/assets/899801c5-6f05-477e-92c7-f2669526fa03) --------- Co-authored-by: Gar --- lib/index.js | 16 +++++++++++-- test/open.js | 67 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/lib/index.js b/lib/index.js index e147cb8..aa7b55d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -131,9 +131,19 @@ const open = (_args, opts = {}, extra = {}) => { let platform = process.platform // process.platform === 'linux' may actually indicate WSL, if that's the case - // we want to treat things as win32 anyway so the host can open the argument + // open the argument with sensible-browser which is pre-installed + // In WSL, set the default browser using, for example, + // export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" + // or + // export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" + // To permanently set the default browser, add the appropriate entry to your shell's + // RC file, e.g. .bashrc or .zshrc. if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) { - platform = 'win32' + platform = 'wsl' + if (!process.env.BROWSER) { + return Promise.reject( + new Error('Set the BROWSER environment variable to your desired browser.')) + } } let command = options.command @@ -146,6 +156,8 @@ const open = (_args, opts = {}, extra = {}) => { // accidentally interpret the first arg as the title, we stick an empty // string immediately after the start command command = 'start ""' + } else if (platform === 'wsl') { + command = 'sensible-browser' } else if (platform === 'darwin') { command = 'open' } else { diff --git a/test/open.js b/test/open.js index 674e721..a5fa2ae 100644 --- a/test/open.js +++ b/test/open.js @@ -2,6 +2,7 @@ const spawk = require('spawk') const t = require('tap') +const os = require('node:os') const promiseSpawn = require('../lib/index.js') @@ -10,6 +11,8 @@ t.afterEach(() => { spawk.clean() }) +const isWSL = process.platform === 'linux' && os.release().toLowerCase().includes('microsoft') + t.test('process.platform === win32', (t) => { const comSpec = process.env.ComSpec const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform') @@ -118,7 +121,8 @@ t.test('process.platform === linux', (t) => { Object.defineProperty(process, 'platform', platformDesc) }) - t.test('uses xdg-open in a shell', async (t) => { + // xdg-open is not installed in WSL by default + t.test('uses xdg-open in a shell', { skip: isWSL }, async (t) => { const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) const result = await promiseSpawn.open('https://google.com') @@ -130,7 +134,8 @@ t.test('process.platform === linux', (t) => { t.ok(proc.called) }) - t.test('ignores shell = false', async (t) => { + // xdg-open is not installed in WSL by default + t.test('ignores shell = false', { skip: isWSL }, async (t) => { const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) const result = await promiseSpawn.open('https://google.com', { shell: false }) @@ -154,22 +159,16 @@ t.test('process.platform === linux', (t) => { t.ok(proc.called) }) - t.test('when os.release() includes Microsoft treats as win32', async (t) => { - const comSpec = process.env.ComSpec - process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe' - t.teardown(() => { - process.env.ComSPec = comSpec - }) - + t.test('when os.release() includes Microsoft treats as WSL', async (t) => { const promiseSpawnMock = t.mock('../lib/index.js', { os: { release: () => 'Microsoft', }, }) + const browser = process.env.BROWSER + process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe' - const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe', - ['/d', '/s', '/c', 'start "" https://google.com'], - { shell: false }) + const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) const result = await promiseSpawnMock.open('https://google.com') t.hasStrict(result, { @@ -177,25 +176,23 @@ t.test('process.platform === linux', (t) => { signal: undefined, }) - t.ok(proc.called) - }) - - t.test('when os.release() includes microsoft treats as win32', async (t) => { - const comSpec = process.env.ComSpec - process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe' t.teardown(() => { - process.env.ComSPec = comSpec + process.env.BROWSER = browser }) + t.ok(proc.called) + }) + + t.test('when os.release() includes microsoft treats as WSL', async (t) => { const promiseSpawnMock = t.mock('../lib/index.js', { os: { release: () => 'microsoft', }, }) + const browser = process.env.BROWSER + process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe' - const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe', - ['/d', '/s', '/c', 'start "" https://google.com'], - { shell: false }) + const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) const result = await promiseSpawnMock.open('https://google.com') t.hasStrict(result, { @@ -203,9 +200,35 @@ t.test('process.platform === linux', (t) => { signal: undefined, }) + t.teardown(() => { + process.env.BROWSER = browser + }) + t.ok(proc.called) }) + t.test('fails on WSL if BROWSER is not set', async (t) => { + const promiseSpawnMock = t.mock('../lib/index.js', { + os: { + release: () => 'microsoft', + }, + }) + const browser = process.env.BROWSER + delete process.env.BROWSER + + const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) + + await t.rejects(promiseSpawnMock.open('https://google.com'), { + message: 'Set the BROWSER environment variable to your desired browser.', + }) + + t.teardown(() => { + process.env.BROWSER = browser + }) + + t.notOk(proc.called) + }) + t.end() })