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
16 changes: 13 additions & 3 deletions docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ The `dev` command starts a development server with hot module replacement at [ht
| `--dotenv=<path>...` | | Path to `.env` file to load, relative to the root directory. Can be repeated, with later files taking precedence. |
| `--envName=<environment>` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server) |
| `-e, --extends=<layer-name>...` | | Extend from a Nuxt layer |
| `--inspect` | | Enable the Node.js inspector for the process serving your app (`--inspect=[host:]port`) |
| `--inspect-brk` | | Enable the Node.js inspector and wait for a debugger to attach (`--inspect-brk=[host:]port`) |
| `--inspect` | | Enable the Node.js inspector for server code (`--inspect=[host:]port`), and for the CLI process on the next port |
| `--inspect-brk` | | Like `--inspect`, and wait for a debugger to attach to the CLI process before loading Nuxt (`--inspect-brk=[host:]port`) |
| `--tui` | `true` | Interactive terminal UI (pinned status panel, folded logs and single-key shortcuts) |
| `--no-tui` | | Disable the interactive terminal UI and stream logs instead |
| `--clear` | `false` | Clear console on restart |
Expand Down Expand Up @@ -116,7 +116,17 @@ Node does not read your system trust store, so requests made from Node to a serv

## Debugging

`--inspect` opens the Node.js inspector on the process actually serving your app, and `--inspect-brk` waits for a debugger to attach before running. Both accept an optional `[host:]port`.
`--inspect` opens the Node.js inspector for your server code: server routes, middleware and server-side rendering, which run in a Nitro worker thread. It accepts an optional `[host:]port` and defaults to `127.0.0.1:9229`, so Chrome DevTools (`chrome://inspect`) and most editors find it without configuration. Your debugger reconnects automatically when the server reloads.

### Debugging the CLI

`nuxt.config`, modules and build hooks run in the CLI process instead, which gets its own inspector on the next port (`9230` by default). Add `localhost:9230` as a target in `chrome://inspect` (**Configure...**) or point your editor at it.

To debug code that runs while Nuxt is loading, use `--inspect-brk`. The CLI process then waits for a debugger to attach to the next port before loading Nuxt:

```bash
npx nuxt dev --inspect-brk
```

`--profile` writes a V8 CPU profile to `nuxt-dev.cpuprofile` in your project when the process exits. `--profile=verbose` also prints a full report to the console.

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 @@ -48,11 +48,11 @@ const command = defineCommand({
...extendsArgs,
'inspect': {
type: 'boolean',
description: 'Enable the Node.js inspector for the process serving your app (`--inspect=[host:]port`)',
description: 'Enable the Node.js inspector for server code (`--inspect=[host:]port`), and for the CLI process on the next port',
},
'inspect-brk': {
type: 'boolean',
description: 'Enable the Node.js inspector and wait for a debugger to attach (`--inspect-brk=[host:]port`)',
description: 'Like `--inspect`, and wait for a debugger to attach to the CLI process before loading Nuxt (`--inspect-brk=[host:]port`)',
},
'tui': {
type: 'boolean',
Expand Down
95 changes: 92 additions & 3 deletions packages/nuxt-cli/src/dev/inspect.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Session } from 'node:inspector'
import process from 'node:process'
import { styleText } from 'node:util'
import { debug, logger } from '../utils/logger'
Expand Down Expand Up @@ -86,11 +87,13 @@ function toPort(value: string): number | undefined {
}

/**
* Open the inspector in the current process, or move it to the requested
* address if Node already opened one via `execArgv`.
* Open the inspector for the nitro dev server worker on the requested address,
* and the inspector for this process on the next port. Node's own inspector
* from `execArgv` is moved there too.
*/
export async function openInspector(options: InspectOptions): Promise<void> {
export async function openInspector(inspectOptions: InspectOptions): Promise<void> {
const inspector = await import('node:inspector')
const options = resolveProcessInspectOptions(inspectOptions)

try {
if (inspector.url()) {
Expand All @@ -102,11 +105,97 @@ export async function openInspector(options: InspectOptions): Promise<void> {
catch (error) {
logger.warn(`Could not start the inspector on ${styleText('cyan', `${options.host}:${options.port}`)}: ${error instanceof Error ? error.message : error}`)
}

inspectDevWorkers(inspector.Session, { ...inspectOptions, wait: false })
}

let workerSession: Session | undefined

/**
* Inspector address for the CLI process itself, which loads `nuxt.config` and
* modules. Server code runs in a nitro worker thread on the requested port.
*/
export function resolveProcessInspectOptions(options: InspectOptions): InspectOptions {
return { ...options, port: options.port === 0 ? 0 : options.port + 1 }
}

/**
* Runs inside a worker thread, so it must be self-contained. Waits for the
* port to be released by the worker it replaces before opening the inspector.
*/
function openWorkerInspector(host: string, port: number): void {
// eslint-disable-next-line node/prefer-global/process
const proc = (globalThis as any).process
const { workerData } = proc.getBuiltinModule('node:worker_threads')
if (!proc.env.NITRO_DEV_WORKER_ID && typeof workerData?.name !== 'string') {
return
}
const inspector = proc.getBuiltinModule('node:inspector')
const { createServer } = proc.getBuiltinModule('node:net')
const deadline = Date.now() + 10_000
const retry = (): void => {
if (Date.now() < deadline) {
setTimeout(attempt, 100).unref()
}
}
const open = (): void => {
try {
inspector.open(port, host, false)
}
catch {}
if (!inspector.url()) {
retry()
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
function attempt(): void {
if (port === 0) {
return open()
}
const probe = createServer()
probe.unref()
probe.once('error', (error: any) => {
if (error?.code === 'EADDRINUSE') {
return retry()
}
proc.stderr.write(`Could not start the inspector on ${host}:${port}: ${error?.message ?? error}\n`)
})
probe.listen(port, host, () => probe.close(open))
}
attempt()
}

/** Open an inspector inside each nitro dev worker thread this process starts. */
export function inspectDevWorkers(SessionConstructor: typeof Session, options: InspectOptions): void {
try {
workerSession?.disconnect()
const session = new SessionConstructor()
session.connect()
const expression = `(${openWorkerInspector.toString()})(${JSON.stringify(options.host)}, ${options.port})`
session.on('NodeWorker.attachedToWorker', ({ params }) => {
const { sessionId } = params
const message = JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression } })
session.post('NodeWorker.sendMessageToWorker', { sessionId, message }, (error) => {
if (error) {
debug(`Could not open the inspector in a worker: ${error.message}`)
}
})
})
session.on('NodeWorker.receivedMessageFromWorker', ({ params }) => {
session.post('NodeWorker.detach', { sessionId: params.sessionId }, () => {})
})
session.post('NodeWorker.enable', { waitForDebuggerOnStart: false }, () => {})
workerSession = session
}
catch (error) {
debug(`Could not watch worker threads for the inspector: ${error}`)
}
}

/** Release the inspector port so another process (a fork) can bind to it. */
export async function closeInspector(): Promise<void> {
try {
workerSession?.disconnect()
workerSession = undefined
const inspector = await import('node:inspector')
if (inspector.url()) {
inspector.close()
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt-cli/test/unit/help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ describe('help', () => {
--dotenv=<path>... Path to \`.env\` file to load, relative to the root directory. Can be repeated, with later files taking precedence.
--envName=<environment> The environment to use when resolving configuration overrides (default is \`production\` when building, and \`development\` when running the dev server)
-e, --extends=<layer-name>... Extend from a Nuxt layer
--inspect Enable the Node.js inspector for the process serving your app (\`--inspect=[host:]port\`)
--inspect-brk Enable the Node.js inspector and wait for a debugger to attach (\`--inspect-brk=[host:]port\`)
--inspect Enable the Node.js inspector for server code (\`--inspect=[host:]port\`), and for the CLI process on the next port
--inspect-brk Like \`--inspect\`, and wait for a debugger to attach to the CLI process before loading Nuxt (\`--inspect-brk=[host:]port\`)
--tui Interactive terminal UI (pinned status panel, folded logs and single-key shortcuts) (Default: true)
--no-tui Disable the interactive terminal UI and stream logs instead
--clear Clear console on restart (Default: false)
Expand Down
72 changes: 71 additions & 1 deletion packages/nuxt-cli/test/unit/inspect.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Session } from 'node:inspector'
import { createServer } from 'node:net'
import process from 'node:process'
import { Worker } from 'node:worker_threads'
import { describe, expect, it } from 'vitest'

import { parseInspectArgs } from '../../src/dev/inspect'
import { closeInspector, inspectDevWorkers, parseInspectArgs, resolveProcessInspectOptions } from '../../src/dev/inspect'

describe('parseInspectArgs', () => {
it('should return undefined when the inspector is not requested', () => {
Expand Down Expand Up @@ -59,3 +63,69 @@ describe('parseInspectArgs', () => {
expect(parseInspectArgs(['--inspect=99999'])).toStrictEqual({ host: '127.0.0.1', port: 9229, wait: false })
})
})

describe('resolveProcessInspectOptions', () => {
it('should use the next port', () => {
expect(resolveProcessInspectOptions({ host: '0.0.0.0', port: 9229, wait: true })).toStrictEqual({ host: '0.0.0.0', port: 9230, wait: true })
})

it('should keep a random port random', () => {
expect(resolveProcessInspectOptions({ host: '127.0.0.1', port: 0, wait: false }).port).toBe(0)
})
})

describe('inspectDevWorkers', () => {
const getFreePort = () => new Promise<number>((resolve) => {
const server = createServer().listen(0, '127.0.0.1', () => {
const { port } = server.address() as { port: number }
server.close(() => resolve(port))
})
})

const workerInspectorURL = (env: Record<string, string>) => {
const worker = new Worker(
`const { parentPort } = require('node:worker_threads')
parentPort.on('message', () => parentPort.postMessage(require('node:inspector').url() ?? null))`,
{ eval: true, env: { ...process.env, ...env } },
)
const deadline = Date.now() + 2000
return new Promise<string | null>((resolve) => {
const poll = () => worker.postMessage('url')
worker.on('message', (url) => {
if (url || Date.now() > deadline) {
worker.terminate().then(() => resolve(url))
}
else {
setTimeout(poll, 50)
}
})
poll()
})
}

it('should open an inspector in nitro dev workers', async () => {
const port = await getFreePort()
inspectDevWorkers(Session, { host: '127.0.0.1', port, wait: false })
try {
expect(await workerInspectorURL({ NITRO_DEV_WORKER_ID: '1' })).toMatch(`ws://127.0.0.1:${port}/`)
expect(await workerInspectorURL({})).toBeNull()
}
finally {
await closeInspector()
}
})

it('should wait for the port to be released', async () => {
const port = await getFreePort()
const blocker = createServer()
await new Promise<void>(resolve => blocker.listen(port, '127.0.0.1', resolve))
setTimeout(() => blocker.close(), 300)
inspectDevWorkers(Session, { host: '127.0.0.1', port, wait: false })
try {
expect(await workerInspectorURL({ NITRO_DEV_WORKER_ID: '1' })).toMatch(`ws://127.0.0.1:${port}/`)
}
finally {
await closeInspector()
}
})
})
Loading