Skip to content

Commit 0cf1306

Browse files
committed
fix(preview): handle listen options and output fallback
1 parent a7add36 commit 0cf1306

3 files changed

Lines changed: 169 additions & 25 deletions

File tree

‎packages/nuxt-cli/src/commands/preview.ts‎

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,51 +25,54 @@ const command = defineCommand({
2525
...extendsArgs,
2626
port: {
2727
type: 'string',
28-
description: 'Port to listen on',
28+
description: 'Port to listen on (default: `NUXT_PORT || NITRO_PORT || PORT`)',
2929
alias: ['p'],
3030
},
31+
host: {
32+
type: 'string',
33+
description: 'Host to listen on (default: `NUXT_HOST || NITRO_HOST || HOST`)',
34+
alias: ['h'],
35+
},
3136
...dotEnvArgs,
3237
},
3338
asyncrun(ctx){
3439
process.env.NODE_ENV=process.env.NODE_ENV||'production'
3540

3641
constcwd=resolveRootDir(ctx.args)
3742

38-
const{ loadNuxt }=awaitloadKit(cwd)
39-
40-
// Loading Nuxt applies the dotenv file to `process.env` as a side effect, so
41-
// the preview server inherits the same variables the dev/build commands see.
4243
letenvLoaded=false
44+
letresolvedOutputDir: string|undefined
4345

44-
constresolvedOutputDir=awaitnewPromise<string>((res)=>{
45-
loadNuxt({
46+
try{
47+
const{ loadNuxt }=awaitloadKit(cwd)
48+
constnuxt=awaitloadNuxt({
4649
cwd,
4750
dotenv: {
4851
cwd,
4952
fileName: ctx.args.dotenv,
5053
},
51-
envName: ctx.args.envName,// nuxt will fall back to NODE_ENV
54+
envName: ctx.args.envName,
5255
ready: true,
5356
overrides: {
5457
...(ctx.args.extends&&{extends: ctx.args.extends}),
5558
modules: [
5659
function(_,nuxt){
5760
envLoaded=true
5861
nuxt.hook('nitro:init',(nitro)=>{
59-
res(resolve(nuxt.options.srcDir||cwd,nitro.options.output.dir||'.output','nitro.json'))
62+
resolvedOutputDir=resolve(nuxt.options.srcDir||cwd,nitro.options.output.dir||'.output','nitro.json')
6063
})
6164
},
6265
],
6366
},
6467
})
65-
.then(nuxt=>nuxt.close())
66-
.catch(()=>{})
67-
.finally(()=>res(''))
68-
})
69-
70-
constdefaultOutput=resolve(cwd,'.output','nitro.json')// for backwards compatibility
68+
awaitnuxt.close()
69+
}
70+
catch{}
7171

72-
constnitroJSONPaths=[resolvedOutputDir,defaultOutput].filter(Boolean)
72+
constnitroJSONPaths=[...newSet([
73+
resolvedOutputDir,
74+
resolve(cwd,'.output','nitro.json'),
75+
].filter((path): path is string=>!!path))]
7376
constnitroJSONPath=nitroJSONPaths.find(p=>existsSync(p))
7477
if(!nitroJSONPath){
7578
logger.error(
@@ -80,7 +83,8 @@ const command = defineCommand({
8083
constoutputPath=dirname(nitroJSONPath)
8184
constnitroJSON=JSON.parse(awaitfsp.readFile(nitroJSONPath,'utf-8'))
8285

83-
if(!nitroJSON.commands.preview){
86+
constpreviewCommand=nitroJSON.commands?.preview
87+
if(typeofpreviewCommand!=='string'||!previewCommand.trim()){
8488
logger.error('Preview is not supported for this build.')
8589
process.exit(1)
8690
}
@@ -97,7 +101,7 @@ const command = defineCommand({
97101
[
98102
'',
99103
'You are previewing a Nuxt app. In production, do not use this CLI. ',
100-
`Instead, run ${styleText('cyan',nitroJSON.commands.preview)} directly.`,
104+
`Instead, run ${styleText('cyan',previewCommand)} directly.`,
101105
'',
102106
...info.map(
103107
([label,value])=>
@@ -139,13 +143,17 @@ const command = defineCommand({
139143
}
140144

141145
constport=ctx.args.port
142-
??process.env.NUXT_PORT
143-
??process.env.NITRO_PORT
144-
??process.env.PORT
146+
||process.env.NUXT_PORT
147+
||process.env.NITRO_PORT
148+
||process.env.PORT
149+
consthost=ctx.args.host
150+
||process.env.NUXT_HOST
151+
||process.env.NITRO_HOST
152+
||process.env.HOST
145153

146-
outro(`Running ${styleText('cyan',nitroJSON.commands.preview)} in ${styleText('cyan',relativeToProcess(outputPath))}`)
154+
outro(`Running ${styleText('cyan',previewCommand)} in ${styleText('cyan',relativeToProcess(outputPath))}`)
147155

148-
const[command, ...commandArgs]=nitroJSON.commands.preview.split(' ')
156+
const[command, ...commandArgs]=previewCommand.trim().split(/\s+/)as[string, ...string[]]
149157
awaitx(command,commandArgs,{
150158
throwOnError: true,
151159
nodeOptions: {
@@ -155,6 +163,8 @@ const command = defineCommand({
155163
...process.env,
156164
NUXT_PORT: port,
157165
NITRO_PORT: port,
166+
NUXT_HOST: host,
167+
NITRO_HOST: host,
158168
},
159169
},
160170
})
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import{mkdir,mkdtemp,rm,writeFile}from'node:fs/promises'
2+
import{tmpdir}from'node:os'
3+
4+
import{runCommand}from'citty'
5+
import{join}from'pathe'
6+
import{afterEach,beforeEach,describe,expect,it,vi}from'vitest'
7+
8+
importpreviewfrom'../../../src/commands/preview'
9+
10+
const{ loadKit, loadNuxt, x }=vi.hoisted(()=>({
11+
loadKit: vi.fn(),
12+
loadNuxt: vi.fn(),
13+
x: vi.fn(),
14+
}))
15+
16+
vi.mock('../../../src/utils/kit',()=>({ loadKit }))
17+
vi.mock('tinyexec',()=>({ x }))
18+
19+
letcwd: string
20+
21+
asyncfunctionwriteNitroJSON(outputDir: string,data: Record<string,unknown>={}){
22+
awaitmkdir(outputDir,{recursive: true})
23+
awaitwriteFile(join(outputDir,'nitro.json'),JSON.stringify({
24+
preset: 'node-server',
25+
commands: {preview: 'node ./server/index.mjs'},
26+
...data,
27+
}))
28+
}
29+
30+
describe('preview',()=>{
31+
beforeEach(async()=>{
32+
cwd=awaitmkdtemp(join(tmpdir(),'nuxt-preview-test-'))
33+
loadKit.mockResolvedValue({ loadNuxt })
34+
loadNuxt.mockImplementation(async(options)=>{
35+
constnuxt={
36+
options: {srcDir: cwd},
37+
hook: vi.fn((name,callback)=>{
38+
if(name==='nitro:init'){
39+
callback({options: {output: {dir: '.output'}}})
40+
}
41+
}),
42+
close: vi.fn(),
43+
}
44+
options.overrides.modules[0](undefined,nuxt)
45+
returnnuxt
46+
})
47+
x.mockResolvedValue({exitCode: 0})
48+
})
49+
50+
afterEach(async()=>{
51+
vi.unstubAllEnvs()
52+
vi.clearAllMocks()
53+
awaitrm(cwd,{recursive: true,force: true})
54+
})
55+
56+
it('runs the build preview command with normalized whitespace and listen options',async()=>{
57+
constoutputDir=join(cwd,'.output')
58+
awaitwriteNitroJSON(outputDir)
59+
60+
awaitrunCommand(preview,{
61+
rawArgs: [cwd,'--port=4321','--host=127.0.0.1'],
62+
})
63+
64+
expect(x).toHaveBeenCalledWith('node',['./server/index.mjs'],{
65+
throwOnError: true,
66+
nodeOptions: expect.objectContaining({
67+
cwd: outputDir,
68+
env: expect.objectContaining({
69+
NUXT_PORT: '4321',
70+
NITRO_PORT: '4321',
71+
NUXT_HOST: '127.0.0.1',
72+
NITRO_HOST: '127.0.0.1',
73+
}),
74+
}),
75+
})
76+
})
77+
78+
it('uses the configured output directory',async()=>{
79+
constoutputDir=join(cwd,'dist','server-output')
80+
awaitwriteNitroJSON(outputDir)
81+
loadNuxt.mockImplementation(async(options)=>{
82+
constnuxt={
83+
options: {srcDir: join(cwd,'src')},
84+
hook: vi.fn((name,callback)=>{
85+
if(name==='nitro:init'){
86+
callback({options: {output: {dir: '../dist/server-output'}}})
87+
}
88+
}),
89+
close: vi.fn(),
90+
}
91+
options.overrides.modules[0](undefined,nuxt)
92+
returnnuxt
93+
})
94+
95+
awaitrunCommand(preview,{rawArgs: [cwd]})
96+
97+
expect(x).toHaveBeenCalledWith('node',['./server/index.mjs'],expect.objectContaining({
98+
nodeOptions: expect.objectContaining({cwd: outputDir}),
99+
}))
100+
})
101+
102+
it('falls back to the conventional output when Nuxt cannot load',async()=>{
103+
loadKit.mockRejectedValue(newError('Nuxt is unavailable'))
104+
constoutputDir=join(cwd,'.output')
105+
awaitwriteNitroJSON(outputDir)
106+
107+
awaitrunCommand(preview,{rawArgs: [cwd]})
108+
109+
expect(x).toHaveBeenCalledWith('node',['./server/index.mjs'],expect.objectContaining({
110+
nodeOptions: expect.objectContaining({cwd: outputDir}),
111+
}))
112+
})
113+
114+
it('uses host and port environment variables',async()=>{
115+
vi.stubEnv('NUXT_PORT','4100')
116+
vi.stubEnv('NUXT_HOST','localhost')
117+
awaitwriteNitroJSON(join(cwd,'.output'))
118+
119+
awaitrunCommand(preview,{rawArgs: [cwd]})
120+
121+
expect(x).toHaveBeenCalledWith(expect.any(String),expect.any(Array),expect.objectContaining({
122+
nodeOptions: expect.objectContaining({
123+
env: expect.objectContaining({
124+
NUXT_PORT: '4100',
125+
NITRO_PORT: '4100',
126+
NUXT_HOST: 'localhost',
127+
NITRO_HOST: 'localhost',
128+
}),
129+
}),
130+
}))
131+
})
132+
})

‎packages/nuxt-cli/test/unit/help.spec.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ describe('help', () => {
341341
--logLevel=<silent|info|verbose> Specify build-time log level
342342
--envName=<env_name> The environment to use when resolving configuration overrides (default is \`production\` when building, and \`development\` when running the dev server)
343343
-e, --extends=<layer-name> Extend from a Nuxt layer
344-
-p, --port=<port> Port to listen on
344+
-p, --port=<port> Port to listen on (default: \`NUXT_PORT || NITRO_PORT || PORT\`)
345+
-h, --host=<host> Host to listen on (default: \`NUXT_HOST || NITRO_HOST || HOST\`)
345346
--dotenv=<dotenv> Path to \`.env\` file to load, relative to the root directory
346347
"
347348
`)
@@ -362,7 +363,8 @@ describe('help', () => {
362363
--logLevel=<silent|info|verbose> Specify build-time log level
363364
--envName=<env_name> The environment to use when resolving configuration overrides (default is \`production\` when building, and \`development\` when running the dev server)
364365
-e, --extends=<layer-name> Extend from a Nuxt layer
365-
-p, --port=<port> Port to listen on
366+
-p, --port=<port> Port to listen on (default: \`NUXT_PORT || NITRO_PORT || PORT\`)
367+
-h, --host=<host> Host to listen on (default: \`NUXT_HOST || NITRO_HOST || HOST\`)
366368
--dotenv=<dotenv> Path to \`.env\` file to load, relative to the root directory
367369
"
368370
`)

0 commit comments

Comments
 (0)