Skip to content
Merged
17 changes: 17 additions & 0 deletions packages/nuxi/bin/nuxi.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import inspector from 'node:inspector'
import nodeModule from 'node:module'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
Expand All @@ -23,6 +24,22 @@ globalThis.__nuxt_cli__ = {
startTime: Date.now(),
entry: fileURLToPath(import.meta.url),
devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)),
cpuProfileSession: undefined,
}

if (
process.argv.includes('--profile')
|| process.argv.some(a => a.startsWith('--profile='))
) {
const session = new inspector.Session()
session.connect()
// eslint-disable-next-line antfu/no-top-level-await
await new Promise((resolve) => {
session.post('Profiler.enable', () => {
session.post('Profiler.start', resolve)
})
})
globalThis.__nuxt_cli__.cpuProfileSession = session
}

// eslint-disable-next-line antfu/no-top-level-await
Expand Down
9 changes: 9 additions & 0 deletions packages/nuxi/src/commands/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ export const extendsArgs = {
},
} as const satisfies Record<string, ArgDef>

export const profileArgs = {
profile: {
type: 'string',
description: 'Profile performance. Use --profile for CPU only, --profile=verbose for full report.',
default: undefined as string | undefined,
valueHint: 'verbose',
},
} as const satisfies Record<string, ArgDef>

export const legacyRootDirArgs = {
// cwd falls back to rootDir's default (indirect default)
cwd: {
Expand Down
123 changes: 74 additions & 49 deletions packages/nuxi/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { overrideEnv } from '../utils/env'
import { clearBuildDir } from '../utils/fs'
import { loadKit } from '../utils/kit'
import { logger } from '../utils/logger'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { startCpuProfile, stopCpuProfile } from '../utils/profile'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'

export default defineCommand({
meta: {
Expand All @@ -31,74 +32,98 @@ export default defineCommand({
...dotEnvArgs,
...envNameArgs,
...extendsArgs,
...profileArgs,
...legacyRootDirArgs,
},
async run(ctx) {
overrideEnv('production')

const cwd = resolve(ctx.args.cwd || ctx.args.rootDir)

intro(colors.cyan('Building Nuxt for production...'))
const profileArg = ctx.args.profile
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
if (profileArg) {
await startCpuProfile()
}

try {
intro(colors.cyan('Building Nuxt for production...'))

const kit = await loadKit(cwd)
const kit = await loadKit(cwd)

await showVersions(cwd, kit, ctx.args.dotenv)
const nuxt = await kit.loadNuxt({
cwd,
dotenv: {
await showVersions(cwd, kit, ctx.args.dotenv)

const nuxt = await kit.loadNuxt({
cwd,
fileName: ctx.args.dotenv,
},
envName: ctx.args.envName, // c12 will fall back to NODE_ENV
overrides: {
logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose',
// TODO: remove in 3.8
_generate: ctx.args.prerender,
nitro: {
static: ctx.args.prerender,
preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET,
dotenv: {
cwd,
fileName: ctx.args.dotenv,
},
envName: ctx.args.envName, // c12 will fall back to NODE_ENV
overrides: {
logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose',
// TODO: remove in 3.8
_generate: ctx.args.prerender,
nitro: {
static: ctx.args.prerender,
preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET,
},
...(ctx.args.extends && { extends: ctx.args.extends }),
...ctx.data?.overrides,
...((perfValue || ctx.data?.overrides?.debug) && {
debug: {
...ctx.data?.overrides?.debug,
...(perfValue && { perf: perfValue }),
},
}),
},
...(ctx.args.extends && { extends: ctx.args.extends }),
...ctx.data?.overrides,
},
})
})

let nitro: ReturnType<typeof kit.useNitro> | undefined
// In Bridge, if Nitro is not enabled, useNitro will throw an error
try {
// Use ? for backward compatibility for Nuxt <= RC.10
nitro = kit.useNitro?.()
if (nitro) {
logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`)
let nitro: ReturnType<typeof kit.useNitro> | undefined
// In Bridge, if Nitro is not enabled, useNitro will throw an error
try {
// Use ? for backward compatibility for Nuxt <= RC.10
nitro = kit.useNitro?.()
if (nitro) {
logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`)
}
}
catch {
//
}
}
catch {
//
}

await clearBuildDir(nuxt.options.buildDir)
await clearBuildDir(nuxt.options.buildDir)

await kit.writeTypes(nuxt)
await kit.writeTypes(nuxt)

nuxt.hook('build:error', (err) => {
logger.error(`Nuxt build error: ${err}`)
process.exit(1)
})
nuxt.hook('build:error', async (err) => {
logger.error(`Nuxt build error: ${err}`)
if (profileArg) {
await stopCpuProfile(cwd, 'build')
}
process.exit(1)
})

await kit.buildNuxt(nuxt)
await kit.buildNuxt(nuxt)

if (ctx.args.prerender) {
if (!nuxt.options.ssr) {
logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`)
logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`)
if (ctx.args.prerender) {
if (!nuxt.options.ssr) {
logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`)
logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`)
}
// TODO: revisit later if/when nuxt build --prerender will output hybrid
const dir = nitro?.options.output.publicDir
const publicDir = dir ? relative(process.cwd(), dir) : '.output/public'
outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`)
}
else {
outro('✨ Build complete!')
}
// TODO: revisit later if/when nuxt build --prerender will output hybrid
const dir = nitro?.options.output.publicDir
const publicDir = dir ? relative(process.cwd(), dir) : '.output/public'
outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`)
}
else {
outro('✨ Build complete!')
finally {
if (profileArg) {
await stopCpuProfile(cwd, 'build')
}
}
},
})
6 changes: 4 additions & 2 deletions packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isBun, isTest } from 'std-env'
import { initialize } from '../dev'
import { ForkPool } from '../dev/pool'
import { debug, logger } from '../utils/logger'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'

const startTime: number | undefined = Date.now()
const forkSupported = !isTest && (!isBun || isBunForkSupported())
Expand Down Expand Up @@ -62,6 +62,7 @@ const command = defineCommand({
},
clipboard: { ...listhenArgs.clipboard, default: false },
},
...profileArgs,
sslCert: {
type: 'string',
description: '(DEPRECATED) Use `--https.cert` instead.',
Expand All @@ -84,7 +85,8 @@ const command = defineCommand({
showBanner: true,
})

if (!ctx.args.fork) {
// Disable forking when profiling to capture all activity in one process
if (!ctx.args.fork || ctx.args.profile) {
return {
listener,
close,
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxi/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineCommand } from 'citty'

import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'
import buildCommand from './build'

export default defineCommand({
Expand All @@ -18,6 +18,7 @@ export default defineCommand({
...dotEnvArgs,
...envNameArgs,
...extendsArgs,
...profileArgs,
...legacyRootDirArgs,
},
async run(ctx) {
Expand Down
23 changes: 23 additions & 0 deletions packages/nuxi/src/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './
import process from 'node:process'
import defu from 'defu'
import { overrideEnv } from '../utils/env.ts'
import { startCpuProfile, stopCpuProfile } from '../utils/profile.ts'
import { NuxtDevServer } from './utils'

const start = Date.now()
Expand Down Expand Up @@ -55,11 +56,22 @@ interface InitializeReturn {
export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise<InitializeReturn> {
overrideEnv('development')

const profileArg = devContext.args.profile
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
const perfOverrides = perfValue
? { debug: { perf: perfValue } } as NuxtConfig
: {}

if (profileArg) {
await startCpuProfile()
}

const devServer = new NuxtDevServer({
cwd: devContext.cwd,
overrides: defu(
ctx.data?.overrides,
({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig,
perfOverrides,
),
logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose',
clear: devContext.args.clear,
Expand Down Expand Up @@ -107,6 +119,17 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti
console.debug(`Dev server (internal) initialized in ${Date.now() - start}ms`)
}

if (profileArg) {
for (const signal of [
'exit',
'SIGTERM' /* Graceful shutdown */,
'SIGINT' /* Ctrl-C */,
'SIGQUIT' /* Ctrl-\ */,
] as const) {
process.once(signal, () => stopCpuProfile(devContext.cwd, 'dev'))
}
}

return {
listener: devServer.listener,
close: async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/nuxi/src/dev/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface NuxtDevContext {
dotenv?: string
envName?: string
extends?: string
profile?: string | boolean
}
}

Expand Down
91 changes: 91 additions & 0 deletions packages/nuxi/src/utils/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Session } from 'node:inspector'
import { mkdirSync, writeFileSync } from 'node:fs'
import process from 'node:process'
import { box } from '@clack/prompts'
import { colors } from 'consola/utils'
import { join, relative } from 'pathe'
import { themeColor } from './ascii'

let session: Session | undefined
let profileCount = 0

export async function startCpuProfile(): Promise<void> {
const cli = globalThis.__nuxt_cli__
if (cli?.cpuProfileSession) {
session = cli.cpuProfileSession
delete cli.cpuProfileSession
return
}
const inspector = await import('node:inspector')
session = new inspector.Session()
session.connect()
try {
await new Promise<void>((res, rej) => {
session!.post('Profiler.enable', (err) => {
if (err) {
return rej(err)
}
session!.post('Profiler.start', (err) => {
if (err) {
return rej(err)
}
res()
})
})
})
}
catch (err) {
session.disconnect()
session = undefined
throw err
}
}

export async function stopCpuProfile(outDir: string, command: string): Promise<string | undefined> {
if (!session) {
return
}
const s = session
session = undefined
const count = profileCount++
const outPath = join(outDir, `nuxt-${command}${count ? `-${count}` : ''}.cpuprofile`)
const relativeOutPath = relative(process.cwd(), outPath).replace(/^(?![^.]{1,2}\/)/, './')
try {
await new Promise<any>((resolve, reject) => {
s.post('Profiler.stop', (err, params) => {
if (err) {
return reject(err)
}

if (!params?.profile) {
return resolve(params)
}

try {
mkdirSync(outDir, { recursive: true })
writeFileSync(outPath, JSON.stringify(params.profile))
const nextSteps = [
`CPU profile written to ${colors.cyan(relativeOutPath)}.`,
`Open it in a CPU profile viewer like your IDE, or ${colors.cyan('https://discoveryjs.github.io/cpupro')}.`,
]
box(`\n${nextSteps.map(step => ` › ${step}`).join('\n')}\n`, '', {
contentAlign: 'left',
titleAlign: 'left',
width: 'auto',
titlePadding: 2,
contentPadding: 2,
rounded: true,
withGuide: false,
formatBorder: (text: string) => `${themeColor + text}\x1B[0m`,
})
}
catch {}

resolve(params)
})
})
}
finally {
s.disconnect()
}
}
Loading
Loading