Skip to content

Commit c1c6687

Browse files
committed
feat(dev): respect BROWSER env when opening browser
1 parent 56c1eb7 commit c1c6687

2 files changed

Lines changed: 71 additions & 12 deletions

File tree

packages/nuxt-cli/src/dev/listen.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,20 +236,50 @@ function centerBlock(block: string): string {
236236
return lines.map(line => indent + line).join('\n')
237237
}
238238

239-
function openBrowser(url: string): void {
239+
/**
240+
* Resolve the command that opens `url`, honouring the de facto `BROWSER` and
241+
* `BROWSER_ARGS` environment variables (`BROWSER=none` disables opening).
242+
*/
243+
export function resolveOpenCommand(
244+
url: string,
245+
platform: NodeJS.Platform = process.platform,
246+
env: NodeJS.ProcessEnv = process.env,
247+
): [command: string, args: string[]] | undefined {
248+
const browser = env.BROWSER?.trim()
249+
if (browser === 'none') {
250+
return
251+
}
252+
253+
if (browser) {
254+
const browserArgs = env.BROWSER_ARGS?.trim().split(/\s+/).filter(Boolean) ?? []
255+
return platform === 'darwin' && !browser.includes('/')
256+
? ['open', ['-a', browser, url, ...(browserArgs.length > 0 ? ['--args', ...browserArgs] : [])]]
257+
: [browser, [...browserArgs, url]]
258+
}
259+
240260
// WSL reports itself as Linux but has no X server, so `xdg-open` fails there;
241261
// `cmd.exe` opens the browser on the Windows side instead. WSL1 spells the
242262
// release `Microsoft`, WSL2 `microsoft-standard-WSL2`.
243-
const isWSL = process.platform === 'linux'
244-
&& (!!process.env.WSL_DISTRO_NAME || release().toLowerCase().includes('microsoft'))
245-
246-
const [command, args] = process.platform === 'darwin'
247-
? ['open', [url]]
248-
// `cmd /c start` owns the arcane quoting rules; `""` is a dummy window title
249-
// (otherwise `start` treats a quoted URL as one), and `&`/`^` need escaping.
250-
: process.platform === 'win32' || isWSL
251-
? ['cmd.exe', ['/c', 'start', '""', url.replace(/[&^]/g, '^$&')]]
252-
: ['xdg-open', [url]] satisfies [string, string[]]
263+
const isWSL = platform === 'linux'
264+
&& (!!env.WSL_DISTRO_NAME || release().toLowerCase().includes('microsoft'))
265+
266+
if (platform === 'darwin') {
267+
return ['open', [url]]
268+
}
269+
// `cmd /c start` owns the arcane quoting rules; `""` is a dummy window title
270+
// (otherwise `start` treats a quoted URL as one), and `&`/`^` need escaping.
271+
if (platform === 'win32' || isWSL) {
272+
return ['cmd.exe', ['/c', 'start', '""', url.replace(/[&^]/g, '^$&')]]
273+
}
274+
return ['xdg-open', [url]]
275+
}
276+
277+
function openBrowser(url: string): void {
278+
const resolved = resolveOpenCommand(url)
279+
if (!resolved) {
280+
return
281+
}
282+
const [command, args] = resolved
253283
try {
254284
spawn(command, args, { stdio: 'ignore', detached: true }).on('error', error => debug('Failed to open browser:', error)).unref()
255285
}

packages/nuxt-cli/test/unit/listen.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { networkInterfaces } from 'node:os'
22

33
import { describe, expect, it, vi } from 'vitest'
44

5-
import { getNetworkAddresses } from '../../src/dev/listen'
5+
import { getNetworkAddresses, resolveOpenCommand } from '../../src/dev/listen'
66

77
vi.mock('node:os', () => ({
88
networkInterfaces: vi.fn(),
@@ -33,3 +33,32 @@ describe('getNetworkAddresses', () => {
3333
expect(getNetworkAddresses()).toEqual(['192.168.1.20'])
3434
})
3535
})
36+
37+
describe('resolveOpenCommand', () => {
38+
const url = 'http://localhost:3000/'
39+
40+
it('should use the platform default when `BROWSER` is unset', () => {
41+
expect(resolveOpenCommand(url, 'darwin', {})).toEqual(['open', [url]])
42+
expect(resolveOpenCommand(url, 'linux', {})).toEqual(['xdg-open', [url]])
43+
expect(resolveOpenCommand(url, 'win32', {})).toEqual(['cmd.exe', ['/c', 'start', '""', url]])
44+
})
45+
46+
it('should open via `cmd.exe` on WSL', () => {
47+
expect(resolveOpenCommand(url, 'linux', { WSL_DISTRO_NAME: 'Ubuntu' })).toEqual(['cmd.exe', ['/c', 'start', '""', url]])
48+
})
49+
50+
it('should not open anything for `BROWSER=none`', () => {
51+
expect(resolveOpenCommand(url, 'linux', { BROWSER: 'none' })).toBeUndefined()
52+
})
53+
54+
it('should respect `BROWSER` and `BROWSER_ARGS`', () => {
55+
expect(resolveOpenCommand(url, 'linux', { BROWSER: 'firefox' })).toEqual(['firefox', [url]])
56+
expect(resolveOpenCommand(url, 'linux', { BROWSER: 'firefox', BROWSER_ARGS: '--private-window' })).toEqual(['firefox', ['--private-window', url]])
57+
})
58+
59+
it('should launch a named application with `open -a` on macOS', () => {
60+
expect(resolveOpenCommand(url, 'darwin', { BROWSER: 'Firefox' })).toEqual(['open', ['-a', 'Firefox', url]])
61+
expect(resolveOpenCommand(url, 'darwin', { BROWSER: 'Firefox', BROWSER_ARGS: '--private-window' })).toEqual(['open', ['-a', 'Firefox', url, '--args', '--private-window']])
62+
expect(resolveOpenCommand(url, 'darwin', { BROWSER: '/usr/local/bin/firefox' })).toEqual(['/usr/local/bin/firefox', [url]])
63+
})
64+
})

0 commit comments

Comments
 (0)