import type { Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema' import type { PackageJson } from 'pkg-types' import os from 'node:os' import process from 'node:process' import { styleText } from 'node:util' import { box } from '@clack/prompts' import { defineCommand } from 'citty' import { readPackageJSON } from 'pkg-types' import { isBun, isDeno, isMinimal } from 'std-env' import { writeText } from 'tinyclip' import { version as nuxiVersion } from '../../package.json' import { getBuilder } from '../utils/banner' import { printJson } from '../utils/console' import { formatInfoBox } from '../utils/formatting' import { logger } from '../utils/logger' import { resolveNitroVersion } from '../utils/nitro' import { getNuxtConfig } from '../utils/nuxt-config' import { readDependencyPackageJson } from '../utils/package-json' import { detectPackageManager, getPackageManagerVersion } from '../utils/package-managers' import { resolveRootDir } from '../utils/paths' import { tryResolveNuxt } from '../utils/resolve-nuxt' import { getServerBuilderName } from '../utils/server-build' import { rootDirArgs } from './_shared' const LEADING_SLASH_RE = /^\// /** Server builders other than Nitro released with Nuxt, by each `server.builder` value naming them. */ const NUXT_SERVER_BUILDERS: Record = { 'vite': 'vite', '@nuxt/vite-server': 'vite', } /** JSON key for each display label, so `--json` output stays stable if a label is reworded. */ const JSON_KEYS: Record = { 'Operating system': 'operatingSystem', 'CPU': 'cpu', 'Bun version': 'bunVersion', 'Deno version': 'denoVersion', 'Node.js version': 'nodeVersion', 'nuxt/cli version': 'cliVersion', 'Package manager': 'packageManager', 'Nuxt version': 'nuxtVersion', 'Server builder': 'serverBuilder', 'Builder': 'builder', 'Config': 'config', 'Modules': 'modules', } export default defineCommand({ meta: { name: 'info', description: 'Get information about Nuxt project', }, args: { ...rootDirArgs, json: { type: 'boolean', description: 'Print project info as JSON', }, }, async run(ctx) { const cwd = resolveRootDir(ctx.args) const packageManagerPromise = detectPackageManager(cwd).then(async detected => detected ? `${detected.name}@${await getPackageManagerVersion(detected.name)}` : 'unknown') const [nuxtConfig, projectPkg] = await Promise.all([ getNuxtConfig(cwd), readPackageJSON(cwd).catch(() => ({} as PackageJson)), ]) const { dependencies = {}, devDependencies = {} } = projectPkg const nuxtPath = tryResolveNuxt(cwd) const versions = new Map>() const getDepVersion = (name: string) => { let version = versions.get(name) if (!version) { version = resolveDependencyVersion(name, [cwd, nuxtPath], cwd, projectPkg, dependencies, devDependencies) versions.set(name, version) } return version } const modulesPromise = Promise.all((nuxtConfig.modules || []).map(async (module) => { const name = normalizeConfigModule(module, cwd) if (!name) { return null } const specifier = Array.isArray(module) ? module[0] : module const packageName = typeof specifier === 'string' && getPackageName(specifier) const version = packageName && await getDepVersion(packageName) return version ? `${name}@${version}` : name })) const [modules, nuxtVersion = '-', nitroVersion, packageManager] = await Promise.all([ modulesPromise, getDepVersion('nuxt').then(version => version || getDepVersion('nuxt-nightly')), resolveNitroVersion(cwd, getDepVersion), packageManagerPromise, ]) const configKeys = Object.keys(nuxtConfig).sort() const moduleNames = modules.filter(module => module !== null) const builder = nuxtConfig.builder || 'vite' // Read from the config, without loading Nuxt, the same way as for a loaded app. const usesNitro = getServerBuilderName({ options: nuxtConfig } as unknown as Nuxt) === 'Nitro' const serverBuilder = usesNitro ? nitroVersion ? `nitro@${nitroVersion}` : 'nitro' : await describeServerBuilder(nuxtConfig, getDepVersion) const osType = os.type() const cpus = os.cpus() const builderInfo = typeof builder === 'string' && ['vite', '@nuxt/vite-builder', 'webpack', '@nuxt/webpack-builder', 'rspack', '@nuxt/rspack-builder'].includes(builder) ? getBuilder(cwd, builder) : { name: 'custom', version: '0.0.0' } const infoObj = { 'Operating system': osType === 'Darwin' ? `macOS ${os.release()}` : osType === 'Windows_NT' ? `Windows ${os.release()}` : `${osType} ${os.release()}`, 'CPU': `${cpus[0]?.model || 'unknown'} (${cpus.length} cores)`, ...isBun // @ts-expect-error Bun global ? { 'Bun version': Bun?.version as string } : isDeno // @ts-expect-error Deno global ? { 'Deno version': Deno?.version.deno as string } : { 'Node.js version': process.version as string }, 'nuxt/cli version': nuxiVersion, 'Package manager': packageManager, 'Nuxt version': nuxtVersion, 'Server builder': serverBuilder, 'Builder': builderInfo.name === 'custom' ? 'custom' : `${builderInfo.name.toLowerCase()}@${builderInfo.version}`, 'Config': configKeys.map(key => `\`${key}\``).join(', '), 'Modules': moduleNames.map(name => `\`${name}\``).join(', '), } if (ctx.args.json) { // Arrays come from the source values rather than the rendered string, so a // key or module path containing `, ` stays a single entry. const lists: Record = { config: configKeys, modules: moduleNames } await printJson({ rootDir: nuxtConfig.rootDir || cwd, ...Object.fromEntries(Object.entries(infoObj).map(([label, value]) => { const key = JSON_KEYS[label]! return [key, lists[key] ?? (value?.replaceAll('`', '') || null)] })), nitroVersion: (usesNitro && nitroVersion) || null, }) return } logger.info(`Nuxt root directory: ${styleText('cyan', nuxtConfig.rootDir || cwd)}\n`) const boxStr = formatInfoBox(infoObj) const copyStr = formatMarkdownTable(infoObj) const copied = !isMinimal && await writeText(copyStr).then(() => true).catch(() => false) if (copied) { box( `\n${boxStr}`, ` Nuxt project info ${styleText('gray', '(copied to clipboard) ')}`, { contentAlign: 'left', titleAlign: 'left', width: 'auto', titlePadding: 2, contentPadding: 2, rounded: true, }, ) } else { logger.info(`Nuxt project info:\n${copyStr}`, { withGuide: false }) } logger.info(`👉 Read documentation: ${styleText('cyan', 'https://nuxt.com')}`) logger.info(`👉 Report an issue: ${styleText('cyan', 'https://github.com/nuxt/nuxt/issues/new?template=bug-report.yml')}`, { spacing: 0, }) logger.info(`👉 Suggest an improvement: ${styleText('cyan', 'https://github.com/nuxt/nuxt/discussions/new')}`, { spacing: 0, }) }, }) async function resolveDependencyVersion( name: string, roots: Array, cwd: string, projectPkg: PackageJson, dependencies: Record, devDependencies: Record, ): Promise { for (const root of roots) { if (!root) { continue } const pkg = await readDependencyPackageJson(name, root) if (pkg?.version) { return pkg.version } } const { resolveCatalogEntry } = await import('../utils/catalog') return resolveCatalogEntry(cwd, projectPkg, name)?.specifier ?? dependencies[name] ?? devDependencies[name] } /** Render `nuxt info --json` output as the Markdown table `nuxt info` copies. */ export function formatJsonAsMarkdownTable(json: Record): string { const labels = Object.fromEntries(Object.entries(JSON_KEYS).map(([label, key]) => [key, label])) const info: Record = {} for (const [key, value] of Object.entries(json)) { if (labels[key]) { info[labels[key]] = Array.isArray(value) ? value.map(item => `\`${item}\``).join(', ') : (value as string | null) ?? undefined } } return formatMarkdownTable(info) } export function formatMarkdownTable(info: Record): string { const entries = Object.entries(info).map(([label, value]) => [label, value || '-'] as const) const labelWidth = Math.max(...entries.map(([label]) => label.length + 4)) const valueWidth = Math.max(...entries.map(([, value]) => value.length + (value.includes('`') ? 0 : 2))) const rows = entries.map(([label, value]) => { const formattedValue = value.includes('`') ? value : `\`${value}\`` return `| ${`**${label}**`.padEnd(labelWidth)} | ${formattedValue.padEnd(valueWidth)} |` }) return [ `| ${' '.repeat(labelWidth)} | ${' '.repeat(valueWidth)} |`, `| ${'-'.repeat(labelWidth)} | ${'-'.repeat(valueWidth)} |`, ...rows, '', ].join('\n') } export function getPackageName(name: string): string | undefined { if (name.startsWith('.') || name.startsWith('/') || /^[a-z]:[\\/]/i.test(name) || name.endsWith('()')) { return undefined } const parts = name.split('/') return name.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] } export function normalizeConfigModule( module: NuxtModule | string | false | null | undefined | readonly [(NuxtModule | string | undefined)?, unknown?], rootDir: string, ): string | null { if (!module) { return null } if (typeof module === 'string') { const normalized = module.replaceAll('\\', '/') const normalizedRoot = rootDir.replaceAll('\\', '/').replace(/\/$/, '') const nodeModulesIndex = normalized.lastIndexOf('/node_modules/') if (nodeModulesIndex !== -1) { return normalized.slice(nodeModulesIndex + '/node_modules/'.length) } return normalized.startsWith(`${normalizedRoot}/`) ? normalized.slice(normalizedRoot.length + 1) : normalized.replace(LEADING_SLASH_RE, '') } if (typeof module === 'function') { return `${module.name}()` } if (Array.isArray(module)) { return normalizeConfigModule(module[0], rootDir) } return null } /** * A server builder other than Nitro: its name when it is released with Nuxt, * otherwise the package or path `server.builder` names (with the package's * version), or `custom` for one defined inline. */ async function describeServerBuilder( nuxtConfig: NuxtConfig, getDepVersion: (name: string) => Promise, ): Promise { const configured = (nuxtConfig as { server?: { builder?: unknown } }).server?.builder if (typeof configured !== 'string') { return 'custom' } const nuxtBuilder = NUXT_SERVER_BUILDERS[configured] if (nuxtBuilder) { return nuxtBuilder } const packageName = getPackageName(configured) const version = packageName && await getDepVersion(packageName) return version ? `${configured}@${version}` : configured }