Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ links:

<!--preview-cmd-->
```bash [Terminal]
npx nuxt preview [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--envName=<environment>] [-e, --extends=<layer-name>...] [-p, --port=<port>] [-h, --host=<host>] [--dotenv=<path>...]
npx nuxt preview [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--envName=<environment>] [-e, --extends=<layer-name>...] [-p, --port=<port>] [-h, --host=<host>] [--takeover] [--strictPort] [--dotenv=<path>...]
```
<!--/preview-cmd-->

Expand Down Expand Up @@ -39,9 +39,29 @@ Some Nitro presets do not produce a server that can be run locally. For those, t
| `-e, --extends=<layer-name>...` | | Extend from a Nuxt layer |
| `-p, --port=<port>` | | Port to listen on (default: `NUXT_PORT \|\| NITRO_PORT \|\| PORT`) |
| `-h, --host=<host>` | | Host to listen on (default: `NUXT_HOST \|\| NITRO_HOST \|\| HOST`) |
| `--takeover` | | Stop a preview server already running on this project and take its place |
| `--no-takeover` | | Never stop a preview server already running on this project |
| `--strictPort` | `false` | Exit if the requested port is unavailable instead of using another one |
| `--dotenv=<path>...` | | Path to `.env` file to load, relative to the root directory. Can be repeated, with later files taking precedence. |
<!--/preview-opts-->

## Taking over a running preview

A preview records itself in `node_modules/.cache/nuxt/preview`, so a second `nuxt preview` for the same project deals with the one already running the way [`nuxt dev`](/docs/api/commands/dev#taking-over-a-running-dev-server) deals with a running dev server:

| Running preview was started | New preview is started | What happens |
|-----------------------------|------------------------|--------------|
| without a terminal | without a terminal | It is stopped, and the new one takes its port. |
| without a terminal | in a terminal | You are asked, defaulting to taking it over. |
| in a terminal | without a terminal | The new one exits, saying where the running one is. |
| in a terminal | in a terminal | You are asked, defaulting to not starting. |

Pass `--takeover` to always stop it and start in its place, or `--no-takeover` to never do so. Choosing "Start anyway" at the prompt, or passing a different `--port`, runs a second preview alongside it instead.

When another server, including `nuxt dev`, is using the requested port, the preview automatically uses another free port without stopping that server. Pass `--strictPort` to exit instead. The requested port comes from `--port`, `NUXT_PORT`, `NITRO_PORT`, or `PORT`, defaulting to `3000`.

Port selection applies to static previews and preview commands that run directly with Node.js, Bun, or Deno. Other preview commands choose their own port.

This command sets `process.env.NODE_ENV` to `production`. To override, define `NODE_ENV` in a `.env` file or as command-line argument.

::note
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt-cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { preflight } from '../dev/preflight'
import { formatRestartReason } from '../dev/reason'
import { devShortcutContext } from '../dev/shortcut-context'
import { handleShutdownSignals } from '../dev/shutdown'
import { formatTakeoverRefusal, takeOverDevServer } from '../dev/takeover'
import { formatTakeoverRefusal, takeOverServer } from '../dev/takeover'
import { beginDevUI, setupDevUI, teardownDevUI } from '../dev/tui/controller'
import { replaceCwdArg } from '../utils/args'
import { resolveLockDir } from '../utils/dev-server'
Expand Down Expand Up @@ -183,7 +183,7 @@ const command = defineCommand({

const buildDir = await beforeServing(() => resolveLockDir(cwd))

const takeover = await beforeServing(() => takeOverDevServer(buildDir, {
const takeover = await beforeServing(() => takeOverServer(buildDir, {
requestedPort: parsePort(listenOverrides.port),
takeover: ctx.args.takeover,
}))
Expand Down
118 changes: 100 additions & 18 deletions packages/nuxt-cli/src/commands/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@ import { styleText } from 'node:util'
import { box } from '@clack/prompts'
import { tokenizeArgs } from 'args-tokenizer'
import { defineCommand } from 'citty'
import { checkPort } from 'get-port-please'
import { resolve } from 'pathe'
import { x } from 'tinyexec'

import { parsePort, resolvePort } from '../dev/listen'
import { formatTakeoverRefusal, takeOverServer } from '../dev/takeover'
import { resolveDotenvFileNames } from '../utils/args'
import { ActionableError } from '../utils/errors'
import { loadKit } from '../utils/kit'
import { acquireLock, previewLockDir, updateLock } from '../utils/lockfile'
import { logger, outro } from '../utils/logger'
import { withPrependedPath } from '../utils/path-env'
import { relativeToProcess, resolveRootDir } from '../utils/paths'
import { resolveServerBuild } from '../utils/server-build'
import { findStaticEntry, formatServerURL, previewStaticOutput } from '../utils/static-preview'
import { dotEnvArgs, envNameArgs, extendsArgs, logLevelArgs, rootDirArgs } from './_shared'

// Other preview commands (such as `wrangler dev`) choose their own port.
const PORT_AWARE_RUNTIMES = new Set(['node', 'bun', 'deno'])
const DEFAULT_PORT = 3000

const command = defineCommand({
meta: {
name: 'preview',
Expand All @@ -40,6 +49,16 @@ const command = defineCommand({
valueHint: 'host',
alias: ['h'],
},
takeover: {
type: 'boolean',
description: 'Stop a preview server already running on this project and take its place',
negativeDescription: 'Never stop a preview server already running on this project',
},
strictPort: {
type: 'boolean',
description: 'Exit if the requested port is unavailable instead of using another one',
default: false,
},
...dotEnvArgs,
},
async run(ctx) {
Expand Down Expand Up @@ -106,6 +125,31 @@ const command = defineCommand({
|| process.env.NITRO_HOST
|| process.env.HOST

async function claimPort(): Promise<number> {
const requestedPort = parsePort(port)
const takeover = await takeOverServer(previewLockDir(cwd), {
command: 'preview',
requestedPort,
takeover: ctx.args.takeover,
})
if (takeover.action === 'refused') {
logger.error(formatTakeoverRefusal(takeover.existing, takeover.reason))
process.exit(1)
}
if (takeover.action === 'taken') {
return takeover.port
}

const listenPort = requestedPort ?? DEFAULT_PORT
if (!ctx.args.strictPort) {
return resolvePort(listenPort, host || '')
}
if (listenPort !== 0 && await checkPort(listenPort, host || undefined) === false) {
throw new ActionableError(`Port ${listenPort} is already in use (\`--strictPort\` is enabled).`)
}
return listenPort
}

let previewCommand: string | undefined
let outputPath: string | undefined
let target: readonly [label: string, value: string] | undefined
Expand Down Expand Up @@ -154,7 +198,9 @@ const command = defineCommand({
const entry = findStaticEntry(dir)
if (entry) {
logger.info(`This build has no server, so ${styleText('cyan', relativeToProcess(dir))} is being served statically.`)
const server = await previewStaticOutput({ dir, entry, port, hostname: host })
const listenPort = await claimPort()
const server = await previewStaticOutput({ dir, entry, port: String(listenPort), hostname: host })
recordPreview(cwd, listenPort, host)
outro(`Previewing ${styleText('cyan', relativeToProcess(dir))} at ${styleText('cyan', formatServerURL(server.url))}`)
return
}
Expand All @@ -174,6 +220,10 @@ const command = defineCommand({
// `outputPath` is set whenever a preview command was found.
const previewDir = outputPath!

const [command, ...commandArgs] = tokenizeArgs(previewCommand) as [string, ...string[]]
const listenPort = PORT_AWARE_RUNTIMES.has(command) ? await claimPort() : undefined
const serverPort = listenPort === undefined ? port : String(listenPort)

const info = [
['Node.js:', `v${process.versions.node}`],
...(target ? [target] : []),
Expand Down Expand Up @@ -227,25 +277,57 @@ const command = defineCommand({

outro(`Running ${styleText('cyan', previewCommand)} in ${styleText('cyan', relativeToProcess(previewDir))}`)

const [command, ...commandArgs] = tokenizeArgs(previewCommand) as [string, ...string[]]
await x(command, commandArgs, {
throwOnError: true,
nodeOptions: {
stdio: 'inherit',
cwd: previewDir,
env: {
...withPrependedPath(process.env, [
resolve(previewDir, 'node_modules/.bin'),
resolve(cwd, 'node_modules/.bin'),
]),
NUXT_PORT: port,
NITRO_PORT: port,
NUXT_HOST: host,
NITRO_HOST: host,
const previewLock = listenPort === undefined ? undefined : recordPreview(cwd, listenPort, host)
try {
const server = x(command, commandArgs, {
throwOnError: true,
nodeOptions: {
stdio: 'inherit',
cwd: previewDir,
env: {
...withPrependedPath(process.env, [
resolve(previewDir, 'node_modules/.bin'),
resolve(cwd, 'node_modules/.bin'),
]),
NUXT_PORT: serverPort,
NITRO_PORT: serverPort,
NUXT_HOST: host,
NITRO_HOST: host,
},
},
},
})
})
if (server.pid) {
previewLock?.update(server.pid)
}
await server
}
finally {
previewLock?.release()
}
},
})

export default command

function recordPreview(rootDir: string, port: number, hostname: string | undefined): { update: (serverPid: number) => void, release: () => void } | undefined {
if (port === 0) {
return
}
const lockDir = previewLockDir(rootDir)
const host = hostname || 'localhost'
const info = {
command: 'preview' as const,
cwd: rootDir,
port,
hostname,
url: `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${port}`,
}
const { release } = acquireLock(lockDir, info)
if (!release) {
return
}
return {
release,
update: serverPid => updateLock(lockDir, { ...info, serverPid }),
}
}
2 changes: 1 addition & 1 deletion packages/nuxt-cli/src/dev/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ export function parsePort(value: string | number | undefined): number | undefine
return port
}

async function resolvePort(requestedPort: number | undefined, hostname: string, strictPort?: boolean): Promise<number> {
export async function resolvePort(requestedPort: number | undefined, hostname: string, strictPort?: boolean): Promise<number> {
if (requestedPort === 0) {
return getPort({ random: true, host: hostname || undefined })
}
Expand Down
Loading
Loading