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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ playground-bun*
playground-deno*
playground-node*
packages/nuxt-cli/test/fixtures/.tmp-*
packages/nuxi/src/utils/completions-data.ts
packages/nuxt-cli/src/data/nitro-presets.ts
packages/nuxt-cli/src/data/templates.ts
bench/results
4 changes: 3 additions & 1 deletion bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ Useful flags:
| Flag | Default | Meaning |
| --- | --- | --- |
| `--baseline` | `latest` | Any npm spec for `@nuxt/cli` to treat as the "before", or `ref:<git-ref>` to build the baseline from a commit of this repo (CI uses the PR's base sha) |
| `--suite` | all | Repeatable. One of `startup`, `modules`, `dev`, `restart`, `build`, `footprint` |
| `--suite` | all | Repeatable. One of `startup`, `modules`, `panel`, `dev`, `restart`, `build`, `footprint` |
| `--fixture` | `playground`, `large` | Repeatable. Which fixture to run the project-level suites against |
| `--workdir` | `~/.cache/nuxt-cli-bench` | Where isolated installs and fixtures live |
| `--out` | `bench/results/report.md` | Markdown report path. A `.json` sibling is written alongside |
| `--startup-reps` | `15` | Repetitions for the startup suite |
| `--panel-reps` | `5` | Repetitions for the panel suite |
| `--dev-reps` | `5` | Repetitions for the dev-server suite |
| `--restart-reps` | `5` | Repetitions for the restart suite |
| `--build-reps` | `3` | Repetitions for the build suite |
Expand All @@ -41,6 +42,7 @@ pnpm bench:cli --suite startup --startup-reps 31

- **`startup`** - cold `nuxi --version`, `--help`, and unknown-command time. Catches anything pulled onto the module graph of the fast paths.
- **`modules`** - how many modules each target loads for those same commands, via `bench/lib/module-hook.mjs`. A count rather than a time, so it is stable across machines and is the better regression guard of the two.
- **`panel`** - time from spawn to the first painted `nuxt dev` frame, and until a keypress at that frame is answered.
- **`dev`** - time from spawn to the dev server answering a request.
- **`restart`** - time to serve again after a `nuxt.config.ts` edit, plus a no-op edit case to check that ignored changes stay ignored.
- **`build`** - wall time for `nuxt build`.
Expand Down
2 changes: 1 addition & 1 deletion docs/1.overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The binary is installed as `nuxt`, and also as `nuxi`, `nuxi-ng` and `nuxt-cli`

## Requirements

The CLI runs on Node v22.19+, v24.11+ or v26+, and imports TypeScript configuration natively on those versions. Nuxt 2 and `@nuxt/bridge` are not supported.
The CLI runs on Node v22.21+, v24.11+ or v26+, and imports TypeScript configuration natively on those versions. Nuxt 2 and `@nuxt/bridge` are not supported.

## Running commands

Expand Down
2 changes: 0 additions & 2 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
],
"entry": [
"scripts/*",
"bench/run.ts",
"bench/ab.ts",
"capture/record.ts",
"capture/lib/fetch-stub.mjs"
],
"ignore": [
Expand Down
2 changes: 1 addition & 1 deletion packages/create-nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dist"
],
"engines": {
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
"node": "^22.21.0 || ^24.11.0 || >=26.0.0"
},
"scripts": {
"build": "tsdown",
Expand Down
77 changes: 15 additions & 62 deletions packages/create-nuxt/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { runCommandDef as runCommand } from '../../nuxt-cli/src/run-command'
import { nuxtIcon } from '../../nuxt-cli/src/utils/ascii'
import { fetchJson } from '../../nuxt-cli/src/utils/fetch'
import { formatHeadlessCommand } from '../../nuxt-cli/src/utils/headless'
import { createInstallLog, runInstall, takeUnreportedIgnoredBuilds } from '../../nuxt-cli/src/utils/install'
import { createInstallLog, isVerboseInstall, runInstall, takeUnreportedIgnoredBuilds } from '../../nuxt-cli/src/utils/install'
import { debug, logger } from '../../nuxt-cli/src/utils/logger'
import { classifyNetworkError, describeNetworkError, logNetworkError, probeNetworkError } from '../../nuxt-cli/src/utils/network'
import { detectPackageManager, isPackageManagerName, packageManagerNames } from '../../nuxt-cli/src/utils/package-managers'
Expand Down Expand Up @@ -293,11 +293,6 @@ export default defineCommand({

templateName ||= DEFAULT_TEMPLATE_NAME

if (typeof templateName !== 'string') {
logger.error('Please specify a template!')
process.exit(1)
}

let dir = ctx.args.dir
if (dir === '') {
const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app'
Expand Down Expand Up @@ -434,12 +429,6 @@ export default defineCommand({
})
const nightlyChannelTag = ctx.args.nightly || 'latest'

if (!nightlyChannelTag) {
nightlySpinner.error('Failed to get nightly channel tag')
logger.error(`Error getting nightly channel tag.`)
process.exit(1)
}

const nightlyChannelVersion = response['dist-tags'][nightlyChannelTag]

if (!nightlyChannelVersion) {
Expand Down Expand Up @@ -548,14 +537,13 @@ export default defineCommand({
prompted = true
}

if (!installRequested || skipInstallOnConflict) {
if (!skipInstallOnConflict) {
logger.info('Skipping install dependencies step.')
}
const shouldInstall = installRequested && !skipInstallOnConflict
if (!installRequested) {
logger.info('Skipping install dependencies step.')
}
else {
else if (shouldInstall) {
const installController = new AbortController()
const installLog = createInstallLog({ verbose: isVerbose(ctx.args.logLevel) })
const installLog = createInstallLog({ verbose: isVerboseInstall(ctx.args.logLevel) })
const installSpinner = createSpinner({
indicator: 'timer',
onCancel: () => installController.abort(),
Expand Down Expand Up @@ -715,7 +703,7 @@ export default defineCommand({
const args: string[] = [
...modulesToAdd,
`--cwd=${template.dir}`,
installRequested && !skipInstallOnConflict ? '' : '--skipInstall',
shouldInstall ? '' : '--skipInstall',
`--packageManager=${selectedPackageManager}`,
ctx.args.logLevel ? `--logLevel=${ctx.args.logLevel}` : '',
].filter(Boolean)
Expand Down Expand Up @@ -786,8 +774,7 @@ async function getModuleDependencies(moduleName: string) {
const url = `https://registry.npmjs.org/${moduleName}/latest`
try {
const response = await fetchJson<{ dependencies?: Record<string, string> }>(url)
const dependencies = response.dependencies || {}
return Object.keys(dependencies)
return Object.keys(response.dependencies || {})
}
catch (err) {
logNetworkError(err, { url, level: 'warn', prefix: `Could not get dependencies for ${styleText('cyan', moduleName)}.` })
Expand All @@ -796,28 +783,11 @@ async function getModuleDependencies(moduleName: string) {
}

function filterModules(modules: string[], allDependencies: Record<string, string[]>) {
const result = {
toInstall: [] as string[],
skipped: [] as string[],
const isDependency = (module: string) => modules.some(other => other !== module && allDependencies[other]?.includes(module))
return {
toInstall: modules.filter(module => !isDependency(module)),
skipped: modules.filter(isDependency),
}

for (const module of modules) {
const isDependency = modules.some((otherModule) => {
if (otherModule === module)
return false
const deps = allDependencies[otherModule] || []
return deps.includes(module)
})

if (isDependency) {
result.skipped.push(module)
}
else {
result.toInstall.push(module)
}
}

return result
}

async function getTemplateDependencies(templateDir: string) {
Expand All @@ -827,33 +797,16 @@ async function getTemplateDependencies(templateDir: string) {
return []
}
const packageJson = await readPackageJSON(packageJsonPath)
const directDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
}
const directDepNames = Object.keys(directDeps)
const allDeps = new Set(directDepNames)

const transitiveDepsResults = await Promise.all(
directDepNames.map(dep => getModuleDependencies(dep)),
)

transitiveDepsResults.forEach((deps) => {
deps.forEach(dep => allDeps.add(dep))
})

return [...allDeps]
const directDepNames = Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies })
const transitiveDeps = await Promise.all(directDepNames.map(getModuleDependencies))
return [...new Set([...directDepNames, ...transitiveDeps.flat()])]
}
catch (err) {
logger.warn(`Could not read template dependencies: ${err}`)
return []
}
}

function isVerbose(logLevel?: string) {
return logLevel === 'verbose' || Boolean(process.env.DEBUG)
}

function detectCurrentPackageManager() {
const userAgent = process.env.npm_config_user_agent
if (!userAgent) {
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"dist"
],
"engines": {
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
"node": "^22.21.0 || ^24.11.0 || >=26.0.0"
},
"scripts": {
"build": "tsdown",
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const _main = defineCommand({
})

async function reportUnknownCommand(command: string): Promise<void> {
const { suggestCommand } = await import('../../nuxt-cli/src/utils/suggest-command')
const { suggestCommand } = await import('../../nuxt-cli/src/utils/suggest')
const suggestion = await suggestCommand(command, nuxiCommands.filter(name => !name.startsWith('_')))
if (!suggestion) {
return
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"runtime"
],
"engines": {
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
"node": "^22.21.0 || ^24.11.0 || >=26.0.0"
},
"scripts": {
"build": "tsdown",
Expand Down
40 changes: 15 additions & 25 deletions packages/nuxt-cli/runtime/dev-request-context.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,43 +229,33 @@ function parseRequest(id, label) {
return { id, label: decoded }
}

function withRequest(read, remove, serve) {
let request
try {
request = parseRequest(read(HEADER), read(LABEL_HEADER))
if (request) {
remove(LABEL_HEADER)
}
}
catch {}
return request ? storage.run(request, serve) : serve()
}

function trackRequests(nitroApp) {
const h3App = nitroApp?.h3App
if (typeof h3App?.handler === 'function') {
const handler = h3App.handler
h3App.handler = Object.assign(function (event) {
let request
try {
const headers = event?.node?.req?.headers
request = parseRequest(headers?.[HEADER], headers?.[LABEL_HEADER])
if (request) {
delete headers[LABEL_HEADER]
}
}
catch {}
return request
? storage.run(request, () => handler.call(this, event))
: handler.call(this, event)
const headers = event?.node?.req?.headers
return withRequest(name => headers?.[name], name => delete headers[name], () => handler.call(this, event))
}, handler)
return
}

// Every nitro v3 entry, dev and deployed, serves through `nitroApp.fetch`.
if (typeof nitroApp?.fetch === 'function') {
const fetch = nitroApp.fetch.bind(nitroApp)
nitroApp.fetch = (req, ...args) => {
let request
try {
request = parseRequest(req?.headers?.get?.(HEADER), req?.headers?.get?.(LABEL_HEADER))
if (request) {
req.headers.delete(LABEL_HEADER)
}
}
catch {}
return request
? storage.run(request, () => fetch(req, ...args))
: fetch(req, ...args)
}
nitroApp.fetch = (req, ...args) => withRequest(name => req?.headers?.get?.(name), name => req.headers.delete(name), () => fetch(req, ...args))
}
}

Expand Down
65 changes: 2 additions & 63 deletions packages/nuxt-cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,18 @@ import { ForkPool } from '../dev/pool'
import { preflight } from '../dev/preflight'
import { formatRestartReason } from '../dev/reason'
import { devShortcutContext } from '../dev/shortcut-context'
import { adoptShutdown, SUPERVISOR_SHUTDOWN_TIMEOUT_MS } from '../dev/shutdown'
import { handleShutdownSignals } from '../dev/shutdown'
import { formatTakeoverRefusal, takeOverDevServer } from '../dev/takeover'
import { beginDevUI, setupDevUI, teardownDevUI } from '../dev/tui/controller'
import { replaceCwdArg } from '../utils/args'
import { resolveLockDir } from '../utils/dev-server'
import { summariseActiveResources } from '../utils/hang'
import { debug, logger } from '../utils/logger'
import { resolveRootDir } from '../utils/paths'
import { startupElapsedMs } from '../utils/startup-clock'
import { dotEnvArgs, envNameArgs, extendsArgs, logLevelArgs, profileArgs, rootDirArgs } from './_shared'

const startTime: number | undefined = Date.now()

const SHUTDOWN_NOTICE_MS = 1500
const forkSupported = !isTest && (!isBun || isBunForkSupported())

const command = defineCommand({
Expand Down Expand Up @@ -235,8 +233,7 @@ const command = defineCommand({
const { context: shortcutContext, attach: attachServer, provide } = devShortcutContext()
provide({ clearCaches })
const startingUI = ui ? await setupDevUI(shortcutContext, { ...uiOptions, enabled: true }) : undefined
setupSignalHandlers(() => shortcutContext.close())
adoptShutdown()
handleShutdownSignals(() => shortcutContext.close())

// Evaluating the dev server's graph blocks the loop; let the panel answer
// anything already typed first.
Expand Down Expand Up @@ -479,64 +476,6 @@ async function beforeServing<T>(work: () => Promise<T>): Promise<T> {
}
}

/**
* Shut the dev server down on `SIGINT`/`SIGTERM`.
*
* Registering any listener for these signals (the fork pool and the CPU
* profiler both do) suppresses Node's default exit behaviour, so Ctrl-C would
* otherwise leave the server, its forks and any tunnel running.
*
* Shutdown is given enough time for `close` hooks (nitro plugins closing database
* connections, and so on) to finish; a second Ctrl-C skips the wait.
*/
function setupSignalHandlers(close: () => Promise<void>): void {
let closing = false
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
if (closing) {
process.exit(130)
}
closing = true

// Ctrl-C should always give the terminal back, even if a watcher or an
// open connection stops the graceful shutdown from settling.
const deadline = setTimeout(() => {
const summary = summariseActiveResources()
logger.warn(`The dev server did not shut down within ${SUPERVISOR_SHUTDOWN_TIMEOUT_MS / 1000}s${summary ? `: ${summary}` : ''}. Exiting anyway.`)
process.exit()
}, SUPERVISOR_SHUTDOWN_TIMEOUT_MS)

// Closing can take a while (nitro plugins draining connections, forks
// exiting), so it says so rather than appearing to hang.
void shutdownWithSpinner(async (indicator) => {
const notice = setTimeout(() => {
indicator.update('Cleaning up... press Ctrl-C again to exit immediately')
}, SHUTDOWN_NOTICE_MS)
notice.unref?.()
try {
await close()
}
catch (error) {
console.error(error)
process.exitCode = 1
}
finally {
clearTimeout(notice)
clearTimeout(deadline)
}
}).finally(() => {
process.exit()
})
})
}
}

/** `withSpinner`, loaded on the way out rather than on every `nuxt dev`. */
async function shutdownWithSpinner(work: (indicator: { update: (message: string) => void }) => Promise<void>): Promise<void> {
const { withSpinner } = await import('../utils/spinner')
return withSpinner('Cleaning up', work, { done: 'Stopped the dev server' })
}

function resolveForkPoolSize(): number | undefined {
const raw = process.env.NUXT_DEV_FORK_POOL_SIZE
if (!raw) {
Expand Down
Loading
Loading