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
async run(ctx) {
3439
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
3540

3641
const cwd = resolveRootDir(ctx.args)
3742

38-
const { loadNuxt } = await loadKit(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
let envLoaded = false
44+
let resolvedOutputDir: string | undefined
4345

44-
const resolvedOutputDir = await new Promise<string>((res) => {
45-
loadNuxt({
46+
try {
47+
const { loadNuxt } = await loadKit(cwd)
48+
const nuxt = await loadNuxt({
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-
const defaultOutput = resolve(cwd, '.output', 'nitro.json') // for backwards compatibility
68+
await nuxt.close()
69+
}
70+
catch {}
7171

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

83-
if (!nitroJSON.commands.preview) {
86+
const previewCommand = nitroJSON.commands?.preview
87+
if (typeof previewCommand !== '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
const port = 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+
const host = 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
await x(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+
import preview from '../../../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+
let cwd: string
20+
21+
async function writeNitroJSON(outputDir: string, data: Record<string, unknown> = {}) {
22+
await mkdir(outputDir, { recursive: true })
23+
await writeFile(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 = await mkdtemp(join(tmpdir(), 'nuxt-preview-test-'))
33+
loadKit.mockResolvedValue({ loadNuxt })
34+
loadNuxt.mockImplementation(async (options) => {
35+
const nuxt = {
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+
return nuxt
46+
})
47+
x.mockResolvedValue({ exitCode: 0 })
48+
})
49+
50+
afterEach(async () => {
51+
vi.unstubAllEnvs()
52+
vi.clearAllMocks()
53+
await rm(cwd, { recursive: true, force: true })
54+
})
55+
56+
it('runs the build preview command with normalized whitespace and listen options', async () => {
57+
const outputDir = join(cwd, '.output')
58+
await writeNitroJSON(outputDir)
59+
60+
await runCommand(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+
const outputDir = join(cwd, 'dist', 'server-output')
80+
await writeNitroJSON(outputDir)
81+
loadNuxt.mockImplementation(async (options) => {
82+
const nuxt = {
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+
return nuxt
93+
})
94+
95+
await runCommand(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(new Error('Nuxt is unavailable'))
104+
const outputDir = join(cwd, '.output')
105+
await writeNitroJSON(outputDir)
106+
107+
await runCommand(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+
await writeNitroJSON(join(cwd, '.output'))
118+
119+
await runCommand(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)