Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 19 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,27 @@ 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 anything else is using the port (`3000` unless you pass `--port`), the preview moves to another free port as `nuxt dev` does, or exits if you pass `--strictPort`. This only applies to presets whose server runs directly with Node.js, Bun or Deno; other presets' preview commands choose their own port.
Comment thread
trueberryless marked this conversation as resolved.
Outdated

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 { SUPERVISOR_SHUTDOWN_TIMEOUT_MS } 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 @@ -185,7 +185,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
84 changes: 79 additions & 5 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,8 +277,8 @@ 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, {
const recordServer = listenPort === undefined ? undefined : recordPreview(cwd, listenPort, host)
const server = x(command, commandArgs, {
throwOnError: true,
nodeOptions: {
stdio: 'inherit',
Expand All @@ -238,14 +288,38 @@ const command = defineCommand({
resolve(previewDir, 'node_modules/.bin'),
resolve(cwd, 'node_modules/.bin'),
]),
NUXT_PORT: port,
NITRO_PORT: port,
NUXT_PORT: serverPort,
NITRO_PORT: serverPort,
NUXT_HOST: host,
NITRO_HOST: host,
},
},
})
if (recordServer && server.pid) {
recordServer(server.pid)
}
await server
},
})

export default command

function recordPreview(rootDir: string, port: number, hostname: string | undefined): (serverPid: number) => void {
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 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