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
returnlines.map(line=>indent+line).join('\n')
237237
}
238238

239-
functionopenBrowser(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+
exportfunctionresolveOpenCommand(
244+
url: string,
245+
platform: NodeJS.Platform=process.platform,
246+
env: NodeJS.ProcessEnv=process.env,
247+
): [command: string,args: string[]]|undefined{
248+
constbrowser=env.BROWSER?.trim()
249+
if(browser==='none'){
250+
return
251+
}
252+
253+
if(browser){
254+
constbrowserArgs=env.BROWSER_ARGS?.trim().split(/\s+/).filter(Boolean)??[]
255+
returnplatform==='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-
constisWSL=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+
constisWSL=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+
functionopenBrowser(url: string): void{
278+
constresolved=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+
consturl='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)