Skip to content

Commit 29558d5

Browse files
committed
feat(dev): hint about --host when bound to loopback in docker or wsl
1 parent 98dcb1c commit 29558d5

3 files changed

Lines changed: 186 additions & 9 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import{existsSync,readFileSync}from'node:fs'
2+
import{release}from'node:os'
3+
importprocessfrom'node:process'
4+
5+
letdockerCache: boolean|undefined
6+
letwslKernelCache: boolean|undefined
7+
8+
/** Whether this process is running inside a Docker (or compatible) container. */
9+
exportfunctionisDocker(): boolean{
10+
dockerCache??=existsSync('/.dockerenv')||readTextFile('/proc/self/cgroup').includes('docker')
11+
returndockerCache
12+
}
13+
14+
/**
15+
* Whether this process is running under the Windows Subsystem for Linux.
16+
*
17+
* WSL1 spells the kernel release `Microsoft`, WSL2 `microsoft-standard-WSL2`.
18+
* A custom-kernel WSL2 install has neither, so `/proc/version` is checked too.
19+
*/
20+
exportfunctionisWsl(platform: NodeJS.Platform=process.platform,env: NodeJS.ProcessEnv=process.env): boolean{
21+
if(platform!=='linux'){
22+
returnfalse
23+
}
24+
if(env.WSL_DISTRO_NAME){
25+
returntrue
26+
}
27+
wslKernelCache??=release().toLowerCase().includes('microsoft')
28+
||readTextFile('/proc/version').toLowerCase().includes('microsoft')
29+
returnwslKernelCache
30+
}
31+
32+
/**
33+
* The name of the container-like environment a loopback bind is unreachable
34+
* from, or `undefined` when the dev server is running directly on the host.
35+
*/
36+
exportfunctiondetectIsolatedEnvironment(): string|undefined{
37+
if(isDocker()){
38+
return'the container'
39+
}
40+
if(isWsl()){
41+
return'WSL'
42+
}
43+
}
44+
45+
functionreadTextFile(path: string): string{
46+
try{
47+
returnreadFileSync(path,'utf8')
48+
}
49+
catch{
50+
return''
51+
}
52+
}

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type { Tunnel } from './tunnel'
66
import{spawn}from'node:child_process'
77
import{createServerascreateHttpServer}from'node:http'
88
import{createServerascreateHttpsServer}from'node:https'
9-
import{networkInterfaces,release}from'node:os'
9+
import{networkInterfaces}from'node:os'
1010
importprocessfrom'node:process'
1111

1212
import{getPort}from'get-port-please'
1313
importcolorsfrom'picocolors'
1414

1515
import{debug,logger}from'../utils/logger'
1616
import{resolveCertificate}from'./cert'
17+
import{detectIsolatedEnvironment,isWsl}from'./environment'
1718
import{resolvePortlessURLs}from'./portless'
1819
import{startTunnel}from'./tunnel'
1920

@@ -64,6 +65,7 @@ export interface Listener {
6465
}
6566

6667
constANY_HOSTS=newSet(['','0.0.0.0','::'])
68+
constLOOPBACK_HOSTS=newSet(['localhost','127.0.0.1','::1'])
6769

6870
constHOSTNAME_RE=/^(?!-)[\d.:a-z-]{1,253}(?<!-)$/i
6971

@@ -202,7 +204,11 @@ export async function listen(handler: RequestListener, options: ListenOptions =
202204
lines.push(line(labelColors[type],labels[type],colors.cyan(displayURL),qr&&displayURL===qrURL))
203205
}
204206
if(!anyHost&&!tunnel&&!portless.url){
205-
lines.push(line(colors.magenta,'Network:',colors.gray(`use ${colors.white('--host')} to expose`),false))
207+
constisolated=LOOPBACK_HOSTS.has(hostname) ? detectIsolatedEnvironment() : undefined
208+
consthint=isolated
209+
? `use ${colors.white('--host')} to reach this server from outside ${isolated}`
210+
: `use ${colors.white('--host')} to expose`
211+
lines.push(line(colors.magenta,'Network:',colors.gray(hint),false))
206212
}
207213
if(publicURL&&publicURL!==url&&!urls.some(entry=>entry.url===publicURL)){
208214
lines.push(line(colors.magenta,'Public:',colors.cyan(publicURL),qr&&publicURL===qrURL))
@@ -365,18 +371,14 @@ export function resolveOpenCommand(
365371
: [browser,[...browserArgs,url]]
366372
}
367373

368-
// WSL reports itself as Linux but has no X server, so `xdg-open` fails there;
369-
// `cmd.exe` opens the browser on the Windows side instead. WSL1 spells the
370-
// release `Microsoft`, WSL2 `microsoft-standard-WSL2`.
371-
constisWSL=platform==='linux'
372-
&&(!!env.WSL_DISTRO_NAME||release().toLowerCase().includes('microsoft'))
373-
374374
if(platform==='darwin'){
375375
return['open',[url]]
376376
}
377377
// `cmd /c start` owns the arcane quoting rules; `""` is a dummy window title
378378
// (otherwise `start` treats a quoted URL as one), and `&`/`^` need escaping.
379-
if(platform==='win32'||isWSL){
379+
// WSL reports itself as Linux but has no X server, so `xdg-open` fails there;
380+
// `cmd.exe` opens the browser on the Windows side instead.
381+
if(platform==='win32'||isWsl(platform,env)){
380382
return['cmd.exe',['/c','start','""',url.replace(/[&^]/g,'^$&')]]
381383
}
382384
return['xdg-open',[url]]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import{afterEach,beforeEach,describe,expect,it,vi}from'vitest'
2+
3+
const{ existsSync, readFileSync, release }=vi.hoisted(()=>({
4+
existsSync: vi.fn(()=>false),
5+
readFileSync: vi.fn(()=>''),
6+
release: vi.fn(()=>'6.1.0-generic'),
7+
}))
8+
9+
vi.mock('node:fs',()=>({ existsSync, readFileSync }))
10+
vi.mock('node:os',()=>({ release }))
11+
12+
/** Re-import so the module-level detection caches start empty. */
13+
asyncfunctionloadEnvironment(){
14+
vi.resetModules()
15+
returnimport('../../src/dev/environment')
16+
}
17+
18+
constrealPlatform=process.platform
19+
20+
functionstubPlatform(platform: NodeJS.Platform){
21+
Object.defineProperty(process,'platform',{value: platform,configurable: true})
22+
}
23+
24+
beforeEach(()=>{
25+
existsSync.mockReturnValue(false)
26+
readFileSync.mockReturnValue('')
27+
release.mockReturnValue('6.1.0-generic')
28+
})
29+
30+
afterEach(()=>{
31+
stubPlatform(realPlatform)
32+
vi.clearAllMocks()
33+
})
34+
35+
describe('isDocker',()=>{
36+
it('should detect `/.dockerenv`',async()=>{
37+
existsSync.mockReturnValue(true)
38+
const{ isDocker }=awaitloadEnvironment()
39+
40+
expect(isDocker()).toBe(true)
41+
})
42+
43+
it('should detect a docker cgroup',async()=>{
44+
readFileSync.mockReturnValue('0::/docker/abc123')
45+
const{ isDocker }=awaitloadEnvironment()
46+
47+
expect(isDocker()).toBe(true)
48+
})
49+
50+
it('should cache the result',async()=>{
51+
const{ isDocker }=awaitloadEnvironment()
52+
53+
expect(isDocker()).toBe(false)
54+
existsSync.mockReturnValue(true)
55+
expect(isDocker()).toBe(false)
56+
})
57+
58+
it('should not detect docker on a plain host',async()=>{
59+
const{ isDocker }=awaitloadEnvironment()
60+
61+
expect(isDocker()).toBe(false)
62+
})
63+
})
64+
65+
describe('isWsl',()=>{
66+
it('should detect `WSL_DISTRO_NAME`',async()=>{
67+
const{ isWsl }=awaitloadEnvironment()
68+
69+
expect(isWsl('linux',{WSL_DISTRO_NAME: 'Ubuntu'})).toBe(true)
70+
})
71+
72+
it('should detect a microsoft kernel release',async()=>{
73+
release.mockReturnValue('5.15.0-microsoft-standard-WSL2')
74+
const{ isWsl }=awaitloadEnvironment()
75+
76+
expect(isWsl('linux',{})).toBe(true)
77+
})
78+
79+
it('should detect microsoft in `/proc/version`',async()=>{
80+
readFileSync.mockReturnValue('Linux version 5.15.0 (Microsoft@Microsoft.com)')
81+
const{ isWsl }=awaitloadEnvironment()
82+
83+
expect(isWsl('linux',{})).toBe(true)
84+
})
85+
86+
it('should be false off linux',async()=>{
87+
release.mockReturnValue('5.15.0-microsoft-standard-WSL2')
88+
const{ isWsl }=awaitloadEnvironment()
89+
90+
expect(isWsl('darwin',{WSL_DISTRO_NAME: 'Ubuntu'})).toBe(false)
91+
expect(isWsl('win32',{})).toBe(false)
92+
})
93+
94+
it('should be false on plain linux',async()=>{
95+
const{ isWsl }=awaitloadEnvironment()
96+
97+
expect(isWsl('linux',{})).toBe(false)
98+
})
99+
})
100+
101+
describe('detectIsolatedEnvironment',()=>{
102+
it('should prefer the container over WSL',async()=>{
103+
existsSync.mockReturnValue(true)
104+
release.mockReturnValue('5.15.0-microsoft-standard-WSL2')
105+
const{ detectIsolatedEnvironment }=awaitloadEnvironment()
106+
107+
expect(detectIsolatedEnvironment()).toBe('the container')
108+
})
109+
110+
it('should report WSL',async()=>{
111+
stubPlatform('linux')
112+
release.mockReturnValue('5.15.0-microsoft-standard-WSL2')
113+
const{ detectIsolatedEnvironment }=awaitloadEnvironment()
114+
115+
expect(detectIsolatedEnvironment()).toBe('WSL')
116+
})
117+
118+
it('should report nothing on a plain host',async()=>{
119+
const{ detectIsolatedEnvironment }=awaitloadEnvironment()
120+
121+
expect(detectIsolatedEnvironment()).toBeUndefined()
122+
})
123+
})

0 commit comments

Comments
 (0)