From 159f91b4cecc1b6f8fb1f7748ca4d7e2b8761712 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 15:20:26 +0000 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20use=20clack=20to=20add=20more=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- knip.json | 2 +- packages/create-nuxt/src/main.ts | 3 - packages/nuxi/package.json | 4 +- packages/nuxi/src/commands/add.ts | 26 ++-- packages/nuxi/src/commands/analyze.ts | 28 +++-- packages/nuxi/src/commands/build.ts | 20 +-- packages/nuxi/src/commands/cleanup.ts | 3 + packages/nuxi/src/commands/dev.ts | 6 +- packages/nuxi/src/commands/info.ts | 132 ++++++++++++++------ packages/nuxi/src/commands/init.ts | 107 +++++++++------- packages/nuxi/src/commands/module/add.ts | 2 +- packages/nuxi/src/commands/module/search.ts | 6 +- packages/nuxi/src/commands/prepare.ts | 9 +- packages/nuxi/src/commands/preview.ts | 61 ++++----- packages/nuxi/src/commands/upgrade.ts | 119 +++++++++++------- packages/nuxi/src/dev/pool.ts | 4 +- packages/nuxi/src/main.ts | 7 +- packages/nuxi/src/utils/banner.ts | 69 ++++------ packages/nuxi/src/utils/console.ts | 60 --------- packages/nuxi/src/utils/fs.ts | 5 +- packages/nuxi/src/utils/logger.ts | 6 +- packages/nuxi/src/utils/paths.ts | 8 ++ packages/nuxi/src/utils/versions.ts | 22 ++++ packages/nuxt-cli/package.json | 4 +- packages/nuxt-cli/src/main.ts | 3 - pnpm-lock.yaml | 24 +++- 26 files changed, 409 insertions(+), 331 deletions(-) delete mode 100644 packages/nuxi/src/utils/console.ts create mode 100644 packages/nuxi/src/utils/paths.ts diff --git a/knip.json b/knip.json index 7319ee279..27d4d3858 100644 --- a/knip.json +++ b/knip.json @@ -32,8 +32,8 @@ "@clack/prompts", "c12", "confbox", - "consola", "copy-paste", + "debug", "defu", "exsolve", "fuse.js", diff --git a/packages/create-nuxt/src/main.ts b/packages/create-nuxt/src/main.ts index b3f3b964b..84ee95ef5 100644 --- a/packages/create-nuxt/src/main.ts +++ b/packages/create-nuxt/src/main.ts @@ -4,7 +4,6 @@ import { provider } from 'std-env' import init from '../../nuxi/src/commands/init' import { setupInitCompletions } from '../../nuxi/src/completions-init' -import { setupGlobalConsole } from '../../nuxi/src/utils/console' import { checkEngines } from '../../nuxi/src/utils/engines' import { logger } from '../../nuxi/src/utils/logger' import { description, name, version } from '../package.json' @@ -22,8 +21,6 @@ const _main = defineCommand({ return } - setupGlobalConsole({ dev: false }) - // Check Node.js version and CLI updates in background if (provider !== 'stackblitz') { await checkEngines().catch(err => logger.error(err)) diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index 190ee5e2e..573838639 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -38,13 +38,14 @@ "@nuxt/schema": "^4.2.0", "@nuxt/test-utils": "^3.20.1", "@types/copy-paste": "^2.1.0", + "@types/debug": "^4.1.12", "@types/node": "^24.10.0", "@types/semver": "^7.7.1", "c12": "^3.3.1", "citty": "^0.1.6", "confbox": "^0.2.2", - "consola": "^3.4.2", "copy-paste": "^2.2.0", + "debug": "^4.4.3", "defu": "^6.1.4", "exsolve": "^1.0.7", "fuse.js": "^7.1.0", @@ -61,6 +62,7 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", + "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "rollup": "^4.52.5", "rollup-plugin-visualizer": "^6.0.5", diff --git a/packages/nuxi/src/commands/add.ts b/packages/nuxi/src/commands/add.ts index 8bd0d449e..78140aa65 100644 --- a/packages/nuxi/src/commands/add.ts +++ b/packages/nuxi/src/commands/add.ts @@ -1,11 +1,14 @@ import { existsSync, promises as fsp } from 'node:fs' import process from 'node:process' +import { cancel, intro, outro } from '@clack/prompts' import { defineCommand } from 'citty' import { dirname, extname, resolve } from 'pathe' +import colors from 'picocolors' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' +import { relativeToProcess } from '../utils/paths' import { templates } from '../utils/templates' import { cwdArgs, logLevelArgs } from './_shared' @@ -39,15 +42,16 @@ export default defineCommand({ async run(ctx) { const cwd = resolve(ctx.args.cwd) + intro(colors.cyan('Adding template...')) + const templateName = ctx.args.template // Validate template name if (!templateNames.includes(templateName)) { - logger.error( - `Template ${templateName} is not supported. Possible values: ${Object.keys( - templates, - ).join(', ')}`, - ) + const templateNames = Object.keys(templates).map(name => colors.cyan(name)) + const lastTemplateName = templateNames.pop() + logger.error(`Template ${colors.cyan(templateName)} is not supported.`) + logger.info(`Possible values are ${templateNames.join(', ')} or ${lastTemplateName}.`) process.exit(1) } @@ -59,7 +63,7 @@ export default defineCommand({ : ctx.args.name if (!name) { - logger.error('name argument is missing!') + cancel('name argument is missing!') process.exit(1) } @@ -74,16 +78,15 @@ export default defineCommand({ // Ensure not overriding user code if (!ctx.args.force && existsSync(res.path)) { - logger.error( - `File exists: ${res.path} . Use --force to override or use a different name.`, - ) + logger.error(`File exists at ${colors.cyan(relativeToProcess(res.path))}.`) + logger.info(`Use ${colors.cyan('--force')} to override or use a different name.`) process.exit(1) } // Ensure parent directory exists const parentDir = dirname(res.path) if (!existsSync(parentDir)) { - logger.info('Creating directory', parentDir) + logger.step(`Creating directory ${colors.cyan(relativeToProcess(parentDir))}.`) if (templateName === 'page') { logger.info('This enables vue-router functionality!') } @@ -92,6 +95,7 @@ export default defineCommand({ // Write file await fsp.writeFile(res.path, `${res.contents.trim()}\n`) - logger.info(`🪄 Generated a new ${templateName} in ${res.path}`) + logger.success(`Created ${colors.cyan(relativeToProcess(res.path))}.`) + outro(`Generated a new ${colors.cyan(templateName)}!`) }, }) diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts index 5dece2159..4f9f6b825 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -3,16 +3,19 @@ import type { NuxtAnalyzeMeta } from '@nuxt/schema' import { promises as fsp } from 'node:fs' import process from 'node:process' +import { intro, note, outro, taskLog } from '@clack/prompts' import { defineCommand } from 'citty' import { defu } from 'defu' import { H3, lazyEventHandler } from 'h3-next' import { join, resolve } from 'pathe' +import colors from 'picocolors' import { serve } from 'srvx' import { overrideEnv } from '../utils/env' import { clearDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' +import { relativeToProcess } from '../utils/paths' import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' const indexHtml = ` @@ -65,6 +68,8 @@ export default defineCommand({ const name = ctx.args.name || 'default' const slug = name.trim().replace(/[^\w-]/g, '_') + intro(colors.cyan('Analyzing bundle size...')) + const startTime = Date.now() const { loadNuxt, buildNuxt } = await loadKit(cwd) @@ -105,8 +110,17 @@ export default defineCommand({ filename: join(analyzeDir, 'client.html'), }) + const tasklog = taskLog({ + title: 'Building Nuxt with analysis enabled', + retainLog: false, + limit: 1, + }) + + tasklog.message('Clearing analyze directory...') await clearDir(analyzeDir) + tasklog.message('Building Nuxt...') await buildNuxt(nuxt) + tasklog.success('Build complete') const endTime = Date.now() @@ -121,14 +135,9 @@ export default defineCommand({ } await nuxt.callHook('build:analyze:done', meta) - await fsp.writeFile( - join(analyzeDir, 'meta.json'), - JSON.stringify(meta, null, 2), - 'utf-8', - ) + await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8') - logger.info(`Analyze results are available at: \`${analyzeDir}\``) - logger.warn('Do not deploy analyze results! Use `nuxi build` before deploying.') + note(`${relativeToProcess(analyzeDir)}\n\nDo not deploy analyze results! Use ${colors.cyan('nuxt build')} before deploying.`, 'Analyze results') if (ctx.args.serve !== false && !process.env.CI) { const app = new H3() @@ -139,7 +148,7 @@ export default defineCommand({ return () => new Response(contents, opts) }) - logger.info('Starting stats server...') + logger.step('Starting stats server...') app.use('/client', serveFile(join(analyzeDir, 'client.html'))) app.use('/nitro', serveFile(join(analyzeDir, 'nitro.html'))) @@ -147,5 +156,8 @@ export default defineCommand({ await serve(app).serve() } + else { + outro('✨ Analysis complete!') + } }, }) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index ec2942294..c9c32cd87 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -2,8 +2,10 @@ import type { Nitro } from 'nitropack' import process from 'node:process' +import { intro, outro } from '@clack/prompts' import { defineCommand } from 'citty' import { relative, resolve } from 'pathe' +import colors from 'picocolors' import { showVersions } from '../utils/banner' import { overrideEnv } from '../utils/env' @@ -38,6 +40,8 @@ export default defineCommand({ const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) + intro(colors.cyan('Building Nuxt for production...')) + const kit = await loadKit(cwd) await showVersions(cwd, kit) @@ -67,7 +71,7 @@ export default defineCommand({ // Use ? for backward compatibility for Nuxt <= RC.10 nitro = kit.useNitro?.() if (nitro) { - logger.info(`Building for Nitro preset: \`${nitro.options.preset}\``) + logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) } } catch { @@ -79,7 +83,7 @@ export default defineCommand({ await kit.writeTypes(nuxt) nuxt.hook('build:error', (err) => { - logger.error('Nuxt Build Error:', err) + logger.error(`Nuxt build error: ${err}`) process.exit(1) }) @@ -87,16 +91,16 @@ export default defineCommand({ if (ctx.args.prerender) { if (!nuxt.options.ssr) { - logger.warn( - 'HTML content not prerendered because `ssr: false` was set. You can read more in `https://nuxt.com/docs/getting-started/deployment#static-hosting`.', - ) + 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' - logger.success( - `You can now deploy \`${publicDir}\` to any static hosting!`, - ) + outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) + } + else { + outro('✨ Build complete!') } }, }) diff --git a/packages/nuxi/src/commands/cleanup.ts b/packages/nuxi/src/commands/cleanup.ts index aeb0debf1..2a97e3368 100644 --- a/packages/nuxi/src/commands/cleanup.ts +++ b/packages/nuxi/src/commands/cleanup.ts @@ -2,6 +2,7 @@ import { defineCommand } from 'citty' import { resolve } from 'pathe' import { loadKit } from '../utils/kit' +import { logger } from '../utils/logger' import { cleanupNuxtDirs } from '../utils/nuxt' import { cwdArgs, legacyRootDirArgs } from './_shared' @@ -19,5 +20,7 @@ export default defineCommand({ const { loadNuxtConfig } = await loadKit(cwd) const nuxtOptions = await loadNuxtConfig({ cwd, overrides: { dev: true } }) await cleanupNuxtDirs(nuxtOptions.rootDir, nuxtOptions.buildDir) + + logger.success('Cleanup complete!') }, }) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index c22fe06b0..e9ef9b786 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -12,7 +12,7 @@ import { isBun, isTest } from 'std-env' import { initialize } from '../dev' import { ForkPool } from '../dev/pool' import { overrideEnv } from '../utils/env' -import { logger } from '../utils/logger' +import { debug, logger } from '../utils/logger' import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' const startTime: number | undefined = Date.now() @@ -106,7 +106,7 @@ const command = defineCommand({ onReady((_address) => { pool.startWarming() if (startTime) { - logger.debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) + debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) } }) @@ -124,7 +124,7 @@ const command = defineCommand({ // Handle IPC messages from the fork if (message.type === 'nuxt:internal:dev:ready') { if (startTime) { - logger.debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) + debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) } } else if (message.type === 'nuxt:internal:dev:restart') { diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts index 4155e7691..f048f0b17 100644 --- a/packages/nuxi/src/commands/info.ts +++ b/packages/nuxi/src/commands/info.ts @@ -3,17 +3,20 @@ import type { PackageJson } from 'pkg-types' import os from 'node:os' import process from 'node:process' +import { stripVTControlCharacters } from 'node:util' +import { box } from '@clack/prompts' import { defineCommand } from 'citty' import { copy as copyToClipboard } from 'copy-paste' import { detectPackageManager } from 'nypm' import { resolve } from 'pathe' +import colors from 'picocolors' import { readPackageJSON } from 'pkg-types' -import { splitByCase } from 'scule' -import { isMinimal } from 'std-env' +import { isBun, isDeno, isMinimal } from 'std-env' import { version as nuxiVersion } from '../../package.json' +import { getBuilder } from '../utils/banner' import { tryResolveNuxt } from '../utils/kit' import { logger } from '../utils/logger' import { getPackageManagerVersion } from '../utils/packageManagers' @@ -73,7 +76,7 @@ export default defineCommand({ const nuxtVersion = await getDepVersion('nuxt') || await getDepVersion('nuxt-nightly') || await getDepVersion('nuxt-edge') || await getDepVersion('nuxt3') || '-' const isLegacy = nuxtVersion.startsWith('2') const builder = !isLegacy - ? nuxtConfig.builder /* latest schema */ || '-' + ? nuxtConfig.builder /* latest schema */ || 'vite' : (nuxtConfig as any /* nuxt v2 */).bridge?.vite ? 'vite' /* bridge vite implementation */ : (nuxtConfig as any /* nuxt v2 */).buildModules?.includes('nuxt-vite') @@ -86,58 +89,105 @@ export default defineCommand({ packageManager += `@${getPackageManagerVersion(packageManager)}` } - const infoObj: Record = { - OperatingSystem: os.type(), - NodeVersion: process.version, - NuxtVersion: nuxtVersion, - CLIVersion: nuxiVersion, - NitroVersion: await getDepVersion('nitropack') || await getDepVersion('nitro'), - PackageManager: packageManager ?? 'unknown', - Builder: typeof builder === 'string' ? builder : 'custom', - UserConfig: Object.keys(nuxtConfig) + const osType = os.type() + const builderInfo = typeof builder === 'string' + ? 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': `${os.cpus()[0]?.model || 'unknown'} (${os.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 ?? 'unknown', + 'Nuxt version': nuxtVersion, + 'Nitro version': await getDepVersion('nitropack') || await getDepVersion('nitro'), + 'Builder': builderInfo.name === 'custom' ? 'custom' : `${builderInfo.name.toLowerCase()}@${builderInfo.version}`, + 'Config': Object.keys(nuxtConfig) .map(key => `\`${key}\``) + .sort() .join(', '), - Modules: await listModules(nuxtConfig.modules), + 'Modules': await listModules(nuxtConfig.modules), + ...isLegacy + ? { 'Build modules': await listModules((nuxtConfig as any /* nuxt v2 */).buildModules || []) } + : {}, } - if (isLegacy) { - infoObj.BuildModules = await listModules((nuxtConfig as any /* nuxt v2 */).buildModules || []) - } - - logger.log('Working directory:', cwd) + logger.info(`Nuxt root directory: ${colors.cyan(nuxtConfig.rootDir || cwd)}\n`) - let maxLength = 0 - const entries = Object.entries(infoObj).map(([key, val]) => { - const label = splitByCase(key).join(' ') - if (label.length > maxLength) { - maxLength = label.length + let firstColumnLength = 0 + let ansiFirstColumnLength = 0 + const entries = Object.entries(infoObj).map(([label, val]) => { + if (label.length > firstColumnLength) { + ansiFirstColumnLength = colors.bold(colors.whiteBright(label)).length + 4 + firstColumnLength = label.length + 4 } return [label, val || '-'] as const }) - let infoStr = '' + + // get maximum width of terminal + const terminalWidth = Math.max(process.stdout.columns || 80, firstColumnLength) - 4 /* box padding */ + + // formatted for copy-pasting into an issue + let copyStr = '| | |\n| --- | --- |\n' + let boxStr = '' for (const [label, value] of entries) { - infoStr - += `- ${ - (`${label}: `).padEnd(maxLength + 2) - }${value.includes('`') ? value : `\`${value}\`` - }\n` + if (!isMinimal) { + copyStr += `| **${label}** | ${value.includes('`') ? value : `\`${value}\``} |\n` + } + const formattedValue = value + .replace(/\b@([^, ]+)/g, (_, r) => colors.gray(` ${r}`)) + .replace(/`([^`]*)`/g, (_, r) => r) + + boxStr += (`${colors.bold(colors.whiteBright(label))}: `).padEnd(ansiFirstColumnLength) + + let boxRowLength = firstColumnLength + for (const item of formattedValue.split(', ')) { + const itemLength = stripVTControlCharacters(item).length + 2 + if (boxRowLength + itemLength > terminalWidth) { + boxStr += `\n${' '.repeat(firstColumnLength)}` + boxRowLength = firstColumnLength + } + boxStr += `${item}, ` + boxRowLength += itemLength + } + boxStr = `${boxStr.slice(0, -2)}\n` } - const copied = !isMinimal && await new Promise(resolve => copyToClipboard(infoStr, err => resolve(!err))) + const copied = !isMinimal && await new Promise(resolve => copyToClipboard(copyStr, err => resolve(!err))) + + box( + `\n${boxStr}`, + ` Nuxt project info ${copied ? colors.gray('(copied to clipboard) ') : ''}`, + { + contentAlign: 'left', + titleAlign: 'left', + width: 'auto', + titlePadding: 2, + contentPadding: 2, + rounded: true, + }, + ) const isNuxt3 = !isLegacy - const isBridge = !isNuxt3 && infoObj.BuildModules?.includes('bridge') - + const isBridge = !isNuxt3 && infoObj['Build modules']?.includes('bridge') const repo = isBridge ? 'nuxt/bridge' : 'nuxt/nuxt' - - const log = [ - (isNuxt3 || isBridge) && `👉 Report an issue: https://github.com/${repo}/issues/new?template=bug-report.yml`, - (isNuxt3 || isBridge) && `👉 Suggest an improvement: https://github.com/${repo}/discussions/new`, - `👉 Read documentation: ${(isNuxt3 || isBridge) ? 'https://nuxt.com' : 'https://v2.nuxt.com'}`, - ].filter(Boolean).join('\n') - - const splitter = '------------------------------' - logger.log(`Nuxt project info: ${copied ? '(copied to clipboard)' : ''}\n\n${splitter}\n${infoStr}${splitter}\n\n${log}\n`) + const docsURL = (isNuxt3 || isBridge) ? 'https://nuxt.com' : 'https://v2.nuxt.com' + logger.info(`👉 Read documentation: ${colors.cyan(docsURL)}`) + if (isNuxt3 || isBridge) { + logger.info(`👉 Report an issue: ${colors.cyan(`https://github.com/${repo}/issues/new?template=bug-report.yml`)}`, { + spacing: 0, + }) + logger.info(`👉 Suggest an improvement: ${colors.cyan(`https://github.com/${repo}/discussions/new`)}`, { + spacing: 0, + }) + } }, }) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 767a75848..658e015d5 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -4,13 +4,13 @@ import type { PackageManagerName } from 'nypm' import { existsSync } from 'node:fs' import process from 'node:process' -import { box, cancel, confirm, isCancel, multiselect, outro, select, text } from '@clack/prompts' +import { box, cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' -import { colors } from 'consola/utils' import { downloadTemplate, startShell } from 'giget' import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' import { basename, join, relative, resolve } from 'pathe' +import colors from 'picocolors' import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' import { hasTTY } from 'std-env' @@ -18,6 +18,7 @@ import { x } from 'tinyexec' import { runCommand } from '../run' import { nuxtIcon, themeColor } from '../utils/ascii' import { logger } from '../utils/logger' +import { relativeToProcess } from '../utils/paths' import { cwdArgs, logLevelArgs } from './_shared' import addModuleCommand from './module/add' @@ -167,7 +168,7 @@ export default defineCommand({ process.stdout.write(`\n${nuxtIcon}\n\n`) } - logger.info(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) + intro(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) if (ctx.args.dir === '') { const result = await text({ @@ -186,7 +187,7 @@ export default defineCommand({ const cwd = resolve(ctx.args.cwd) let templateDownloadPath = resolve(cwd, ctx.args.dir) - logger.info(`Creating a new project in ${colors.cyan(relative(cwd, templateDownloadPath) || templateDownloadPath)}.`) + logger.step(`Creating project in ${colors.cyan(relativeToProcess(templateDownloadPath))}`) // Get template name const templateName = ctx.args.template || DEFAULT_TEMPLATE_NAME @@ -245,6 +246,9 @@ export default defineCommand({ // Download template let template: DownloadTemplateResult + const downloadSpinner = spinner() + downloadSpinner.start(`Downloading ${colors.cyan(templateName)} template`) + try { template = await downloadTemplate(templateName, { dir: templateDownloadPath, @@ -273,8 +277,11 @@ export default defineCommand({ } } } + + downloadSpinner.stop(`Downloaded ${colors.cyan(template.name)} template`) } catch (err) { + downloadSpinner.stop('Template download failed', 1) if (process.env.DEBUG) { throw err } @@ -283,12 +290,7 @@ export default defineCommand({ } if (ctx.args.nightly !== undefined && !ctx.args.offline && !ctx.args.preferOffline) { - const response = await $fetch<{ - 'dist-tags': { - [key: string]: string - } - }>('https://registry.npmjs.org/nuxt-nightly') - + const response = await $fetch<{ 'dist-tags': Record }>('https://registry.npmjs.org/nuxt-nightly') const nightlyChannelTag = ctx.args.nightly || 'latest' if (!nightlyChannelTag) { @@ -357,34 +359,7 @@ export default defineCommand({ selectedPackageManager = result } - // Install project dependencies - // or skip installation based on the '--no-install' flag - if (ctx.args.install === false) { - logger.info('Skipping install dependencies step.') - } - else { - logger.start('Installing dependencies...') - - try { - await installDependencies({ - cwd: template.dir, - packageManager: { - name: selectedPackageManager, - command: selectedPackageManager, - }, - }) - } - catch (err) { - if (process.env.DEBUG) { - throw err - } - logger.error((err as Error).toString()) - process.exit(1) - } - - logger.success('Installation completed.') - } - + // Determine if we should init git if (ctx.args.gitInit === undefined) { const result = await confirm({ message: 'Initialize git repository?', @@ -397,18 +372,58 @@ export default defineCommand({ ctx.args.gitInit = result } - if (ctx.args.gitInit) { - logger.info('Initializing git repository...\n') - try { - await x('git', ['init', template.dir], { - throwOnError: true, - nodeOptions: { - stdio: 'inherit', + + // Install project dependencies and initialize git + // or skip installation based on the '--no-install' flag + if (ctx.args.install === false) { + logger.info('Skipping install dependencies step.') + } + else { + const setupTasks: Array<{ title: string, task: () => Promise }> = [ + { + title: `Installing dependencies with ${colors.cyan(selectedPackageManager)}`, + task: async () => { + await installDependencies({ + cwd: template.dir, + packageManager: { + name: selectedPackageManager, + command: selectedPackageManager, + }, + }) + return 'Dependencies installed' + }, + }, + ] + + if (ctx.args.gitInit) { + setupTasks.push({ + title: 'Initializing git repository', + task: async () => { + try { + await x('git', ['init', template.dir], { + throwOnError: true, + nodeOptions: { + stdio: 'inherit', + }, + }) + return 'Git repository initialized' + } + catch (err) { + return `Git initialization failed: ${err}` + } }, }) } + + try { + await tasks(setupTasks) + } catch (err) { - logger.warn(`Failed to initialize git repository: ${err}`) + if (process.env.DEBUG) { + throw err + } + logger.error((err as Error).toString()) + process.exit(1) } } diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index a42a607a0..08391dfca 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -11,10 +11,10 @@ import process from 'node:process' import { confirm, isCancel, select } from '@clack/prompts' import { updateConfig } from 'c12/update' import { defineCommand } from 'citty' -import { colors } from 'consola/utils' import { addDependency, detectPackageManager } from 'nypm' import { $fetch } from 'ofetch' import { resolve } from 'pathe' +import colors from 'picocolors' import { readPackageJSON } from 'pkg-types' import { satisfies } from 'semver' import { joinURL } from 'ufo' diff --git a/packages/nuxi/src/commands/module/search.ts b/packages/nuxi/src/commands/module/search.ts index 701199f16..b8be09b78 100644 --- a/packages/nuxi/src/commands/module/search.ts +++ b/packages/nuxi/src/commands/module/search.ts @@ -1,6 +1,6 @@ import { defineCommand } from 'citty' -import { colors } from 'consola/utils' import Fuse from 'fuse.js' +import colors from 'picocolors' import { kebabCase, upperFirst } from 'scule' import { logger } from '../../utils/logger' @@ -68,7 +68,7 @@ async function findModuleByKeywords(query: string, nuxtVersion: string) { repository: gray(result.item.github), description: gray(result.item.description), package: gray(result.item.npm), - install: cyan(`npx nuxi module add ${result.item.name}`), + install: cyan(`npx nuxt module add ${result.item.name}`), stars: yellow(formatNumber(result.item.stats.stars)), monthlyDownloads: yellow(formatNumber(result.item.stats.downloads)), } @@ -108,6 +108,6 @@ async function findModuleByKeywords(query: string, nuxtVersion: string) { + value }\n` } - logger.log(infoStr) + logger.info(infoStr) } } diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index 7c9ac9a79..d9f4c42c4 100644 --- a/packages/nuxi/src/commands/prepare.ts +++ b/packages/nuxi/src/commands/prepare.ts @@ -1,11 +1,13 @@ import process from 'node:process' import { defineCommand } from 'citty' -import { relative, resolve } from 'pathe' +import { resolve } from 'pathe' +import colors from 'picocolors' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' +import { relativeToProcess } from '../utils/paths' import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ @@ -45,9 +47,6 @@ export default defineCommand({ await buildNuxt(nuxt) await writeTypes(nuxt) - logger.success( - 'Types generated in', - relative(process.cwd(), nuxt.options.buildDir), - ) + logger.success(`Types generated in ${colors.cyan(relativeToProcess(nuxt.options.buildDir))}.`) }, }) diff --git a/packages/nuxi/src/commands/preview.ts b/packages/nuxi/src/commands/preview.ts index afac428ff..523dcd1d5 100644 --- a/packages/nuxi/src/commands/preview.ts +++ b/packages/nuxi/src/commands/preview.ts @@ -1,15 +1,17 @@ import { existsSync, promises as fsp } from 'node:fs' -import { dirname, relative } from 'node:path' +import { dirname } from 'node:path' import process from 'node:process' +import { box, outro } from '@clack/prompts' import { setupDotenv } from 'c12' import { defineCommand } from 'citty' -import { box, colors } from 'consola/utils' import { resolve } from 'pathe' +import colors from 'picocolors' import { x } from 'tinyexec' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' +import { relativeToProcess } from '../utils/paths' import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' const command = defineCommand({ @@ -61,8 +63,7 @@ const command = defineCommand({ const nitroJSONPath = nitroJSONPaths.find(p => existsSync(p)) if (!nitroJSONPath) { logger.error( - 'Cannot find `nitro.json`. Did you run `nuxi build` first? Search path:\n', - nitroJSONPaths, + `Cannot find ${colors.cyan('nitro.json')}. Did you run ${colors.cyan('nuxi build')} first? Search path:\n${nitroJSONPaths.join('\n')}`, ) process.exit(1) } @@ -76,31 +77,35 @@ const command = defineCommand({ const info = [ ['Node.js:', `v${process.versions.node}`], - ['Nitro Preset:', nitroJSON.preset], - ['Working directory:', relative(process.cwd(), outputPath)], + ['Nitro preset:', nitroJSON.preset], + ['Working directory:', relativeToProcess(outputPath)], ] as const const _infoKeyLen = Math.max(...info.map(([label]) => label.length)) - logger.log( - box( - [ - 'You are running Nuxt production build in preview mode.', - `For production deployments, please directly use ${colors.cyan( - nitroJSON.commands.preview, - )} command.`, - '', - ...info.map( - ([label, value]) => - `${label.padEnd(_infoKeyLen, ' ')} ${colors.cyan(value)}`, - ), - ].join('\n'), - { - title: colors.yellow('Preview Mode'), - style: { - borderColor: 'yellow', - }, - }, - ), + logger.message('') + box( + [ + '', + 'You are previewing a Nuxt app. In production, do not use this CLI. ', + `Instead, run ${colors.cyan(nitroJSON.commands.preview)} directly.`, + '', + ...info.map( + ([label, value]) => + `${label.padEnd(_infoKeyLen, ' ')} ${colors.cyan(value)}`, + ), + '', + ].join('\n'), + colors.yellow(' Previewing Nuxt app '), + { + contentAlign: 'left', + titleAlign: 'left', + width: 'auto', + titlePadding: 2, + contentPadding: 2, + rounded: true, + includePrefix: true, + formatBorder: (text: string) => colors.yellow(text), + }, ) const envFileName = ctx.args.dotenv || '.env' @@ -122,9 +127,9 @@ const command = defineCommand({ ?? process.env.NITRO_PORT ?? process.env.PORT - logger.info(`Starting preview command: \`${nitroJSON.commands.preview}\``) + outro(`Running ${colors.cyan(nitroJSON.commands.preview)} in ${colors.cyan(relativeToProcess(outputPath))}`) + const [command, ...commandArgs] = nitroJSON.commands.preview.split(' ') - logger.log('') await x(command, commandArgs, { throwOnError: true, nodeOptions: { diff --git a/packages/nuxi/src/commands/upgrade.ts b/packages/nuxi/src/commands/upgrade.ts index c070c3e8d..64dc38bcb 100644 --- a/packages/nuxi/src/commands/upgrade.ts +++ b/packages/nuxi/src/commands/upgrade.ts @@ -3,17 +3,18 @@ import type { PackageJson } from 'pkg-types' import { existsSync } from 'node:fs' import process from 'node:process' -import { cancel, isCancel, select } from '@clack/prompts' +import { cancel, intro, isCancel, note, outro, select, spinner, tasks } from '@clack/prompts' import { defineCommand } from 'citty' -import { colors } from 'consola/utils' import { addDependency, dedupeDependencies, detectPackageManager } from 'nypm' import { resolve } from 'pathe' +import colors from 'picocolors' import { findWorkspaceDir, readPackageJSON } from 'pkg-types' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt' import { getPackageManagerVersion } from '../utils/packageManagers' +import { relativeToProcess } from '../utils/paths' import { getNuxtVersion } from '../utils/versions' import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared' @@ -39,7 +40,7 @@ function getNightlyDependency(dep: string, nuxtVersion: NuxtVersionTag) { } async function getNightlyVersion(packageNames: string[]): Promise<{ npmPackages: string[], nuxtVersion: NuxtVersionTag }> { - const result = await select({ + const nuxtVersion = await select({ message: 'Which nightly Nuxt release channel do you want to install?', options: [ { value: '3.x' as const, label: '3.x' }, @@ -48,13 +49,11 @@ async function getNightlyVersion(packageNames: string[]): Promise<{ npmPackages: initialValue: '4.x' as const, }) - if (isCancel(result)) { + if (isCancel(nuxtVersion)) { cancel('Operation cancelled.') process.exit(1) } - const nuxtVersion = result as NuxtVersionTag - const npmPackages = packageNames.map(p => getNightlyDependency(p, nuxtVersion)) return { npmPackages, nuxtVersion } @@ -107,21 +106,24 @@ export default defineCommand({ async run(ctx) { const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) + intro(colors.cyan('Upgrading Nuxt ...')) + // Check package manager const [packageManager, workspaceDir = cwd] = await Promise.all([detectPackageManager(cwd), findWorkspaceDir(cwd, { try: true })]) if (!packageManager) { logger.error( - `Unable to determine the package manager used by this project.\n\nNo lock files found in \`${cwd}\`, and no \`packageManager\` field specified in \`package.json\`.\n\nPlease either add the \`packageManager\` field to \`package.json\` or execute the installation command for your package manager. For example, you can use \`pnpm i\`, \`npm i\`, \`bun i\`, or \`yarn i\`, and then try again.`, + `Unable to determine the package manager used by this project.\n\nNo lock files found in ${colors.cyan(relativeToProcess(cwd))}, and no ${colors.cyan('packageManager')} field specified in ${colors.cyan('package.json')}.`, ) + logger.info(`Please either add the ${colors.cyan('packageManager')} field to ${colors.cyan('package.json')} or execute the installation command for your package manager. For example, you can use ${colors.cyan('pnpm i')}, ${colors.cyan('npm i')}, ${colors.cyan('bun i')}, or ${colors.cyan('yarn i')}, and then try again.`) process.exit(1) } const { name: packageManagerName, lockFile: lockFileCandidates } = packageManager const packageManagerVersion = getPackageManagerVersion(packageManagerName) - logger.info('Package manager:', packageManagerName, packageManagerVersion) + logger.step(`Package manager: ${colors.cyan(packageManagerName)} ${packageManagerVersion}`) // Check currently installed Nuxt version const currentVersion = (await getNuxtVersion(cwd, false)) || '[unknown]' - logger.info('Current Nuxt version:', currentVersion) + logger.step(`Current Nuxt version: ${colors.cyan(currentVersion)}`) const pkg = await readPackageJSON(cwd).catch(() => null) @@ -150,7 +152,7 @@ export default defineCommand({ if (!method) { const result = await select({ - message: `Would you like to dedupe your lockfile (recommended) or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.`, + message: `Would you like to dedupe your lockfile, or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.`, options: [ { label: 'dedupe lockfile', @@ -178,56 +180,78 @@ export default defineCommand({ } const versionType = ctx.args.channel === 'nightly' ? 'nightly' : `latest ${ctx.args.channel}` - logger.info(`Installing ${versionType} Nuxt ${nuxtVersion} release...`) - await addDependency(npmPackages, { - cwd, - packageManager, - dev: nuxtDependencyType === 'devDependencies', - workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), - }) + const spin = spinner() + spin.start('Upgrading Nuxt') + + await tasks([ + { + title: `Installing ${versionType} Nuxt ${nuxtVersion} release`, + task: async () => { + await addDependency(npmPackages, { + cwd, + packageManager, + dev: nuxtDependencyType === 'devDependencies', + workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), + }) + return 'Nuxt packages installed' + }, + }, + ...(method === 'force' + ? [{ + title: `Recreating ${forceRemovals}`, + task: async () => { + await dedupeDependencies({ recreateLockfile: true }) + return 'Lockfile recreated' + }, + }] + : []), + ...(method === 'dedupe' + ? [{ + title: 'Deduping dependencies', + task: async () => { + await dedupeDependencies() + return 'Dependencies deduped' + }, + }] + : []), + { + title: 'Cleaning up build directories', + task: async () => { + let buildDir: string = '.nuxt' + try { + const { loadNuxtConfig } = await loadKit(cwd) + const nuxtOptions = await loadNuxtConfig({ cwd }) + buildDir = nuxtOptions.buildDir + } + catch { + // Use default buildDir (.nuxt) + } + await cleanupNuxtDirs(cwd, buildDir) + return 'Build directories cleaned' + }, + }, + ]) + + spin.stop() if (method === 'force') { - logger.info( - `Recreating ${forceRemovals}. If you encounter any issues, revert the changes and try with \`--no-force\``, - ) - await dedupeDependencies({ recreateLockfile: true }) - } - - if (method === 'dedupe') { - logger.info('Try deduping dependencies...') - await dedupeDependencies() - } - - // Clean up after upgrade - let buildDir: string = '.nuxt' - try { - const { loadNuxtConfig } = await loadKit(cwd) - const nuxtOptions = await loadNuxtConfig({ cwd }) - buildDir = nuxtOptions.buildDir - } - catch { - // Use default buildDir (.nuxt) + logger.info(`If you encounter any issues, revert the changes and try with ${colors.cyan('--no-force')}`) } - await cleanupNuxtDirs(cwd, buildDir) // Check installed Nuxt version again const upgradedVersion = (await getNuxtVersion(cwd, false)) || '[unknown]' - logger.info('Upgraded Nuxt version:', upgradedVersion) if (upgradedVersion === '[unknown]') { return } if (upgradedVersion === currentVersion) { - logger.success('You\'re using the latest version of Nuxt.') + outro(`You were already using the latest version of Nuxt (${colors.green(currentVersion)})`) } else { logger.success( - 'Successfully upgraded Nuxt from', - currentVersion, - 'to', - upgradedVersion, + `Successfully upgraded Nuxt from ${colors.cyan(currentVersion)} to ${colors.green(upgradedVersion)}`, ) if (currentVersion === '[unknown]') { return @@ -235,11 +259,12 @@ export default defineCommand({ const commitA = nuxtVersionToGitIdentifier(currentVersion) const commitB = nuxtVersionToGitIdentifier(upgradedVersion) if (commitA && commitB) { - logger.info( - 'Changelog:', + note( `https://github.com/nuxt/nuxt/compare/${commitA}...${commitB}`, + 'Changelog', ) } + outro('✨ Upgrade complete!') } }, }) @@ -253,7 +278,7 @@ function normaliseLockFile(cwd: string, lockFiles: string | Array | unde const lockFile = lockFiles?.find(file => existsSync(resolve(cwd, file))) if (lockFile === undefined) { - logger.error(`Unable to find any lock files in ${cwd}`) + logger.error(`Unable to find any lock files in ${colors.cyan(relativeToProcess(cwd))}.`) return undefined } diff --git a/packages/nuxi/src/dev/pool.ts b/packages/nuxi/src/dev/pool.ts index d58331d76..c4a92ba99 100644 --- a/packages/nuxi/src/dev/pool.ts +++ b/packages/nuxi/src/dev/pool.ts @@ -5,7 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage } from './utils' import { fork } from 'node:child_process' import process from 'node:process' import { isDeno } from 'std-env' -import { logger } from '../utils/logger' +import { debug } from '../utils/logger' interface ForkPoolOptions { rawArgs: string[] @@ -94,7 +94,7 @@ export class ForkPool { } // No forks in pool, create a cold fork - logger.debug('No pre-warmed forks available, starting cold fork') + debug('No pre-warmed forks available, starting cold fork') const coldFork = this.createFork() await coldFork.ready coldFork.state = 'active' diff --git a/packages/nuxi/src/main.ts b/packages/nuxi/src/main.ts index 1f8f4cae2..5ded34fdf 100644 --- a/packages/nuxi/src/main.ts +++ b/packages/nuxi/src/main.ts @@ -11,9 +11,8 @@ import { description, name, version } from '../package.json' import { commands } from './commands' import { cwdArgs } from './commands/_shared' import { initCompletions } from './completions' -import { setupGlobalConsole } from './utils/console' import { checkEngines } from './utils/engines' -import { logger } from './utils/logger' +import { debug, logger } from './utils/logger' // globalThis.crypto support for Node.js 18 if (!globalThis.crypto) { @@ -47,9 +46,7 @@ const _main = defineCommand({ subCommands: commands, async setup(ctx) { const command = ctx.args._[0] - logger.debug(`Running \`nuxt ${command}\` command`) - const dev = command === 'dev' - setupGlobalConsole({ dev }) + debug(`Running \`nuxt ${command}\` command`) // Check Node.js version and CLI updates in background let backgroundTasks: Promise | undefined diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 2b79326ce..4c8f24fcf 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -1,54 +1,37 @@ -import type { NuxtOptions } from '@nuxt/schema' +import type { NuxtBuilder, NuxtConfig, NuxtOptions } from '@nuxt/schema' -import { readFileSync } from 'node:fs' -import { colors } from 'consola/utils' -import { resolveModulePath } from 'exsolve' +import colors from 'picocolors' -import { tryResolveNuxt } from './kit' import { logger } from './logger' - -export function showVersionsFromConfig(cwd: string, config: NuxtOptions) { - const { bold, gray, green } = colors - - function getBuilder(): { name: string, version: string } { - switch (config!.builder) { - case '@nuxt/rspack-builder': - return { name: 'Rspack', version: getPkgVersion('@rspack/core') } - case '@nuxt/webpack-builder': - return { name: 'Webpack', version: getPkgVersion('webpack') } - case '@nuxt/vite-builder': - default: { - const pkgJSON = getPkgJSON('vite') - const isRolldown = pkgJSON.name.includes('rolldown') - return { name: isRolldown ? 'Rolldown-Vite' : 'Vite', version: pkgJSON.version } - } +import { getPkgJSON, getPkgVersion } from './versions' + +export function getBuilder(cwd: string, builder: Exclude): { name: string, version: string } { + switch (builder) { + case 'rspack': + case '@nuxt/rspack-builder': + return { name: 'Rspack', version: getPkgVersion(cwd, '@rspack/core') } + case 'webpack': + case '@nuxt/webpack-builder': + return { name: 'Webpack', version: getPkgVersion(cwd, 'webpack') } + case 'vite': + case '@nuxt/vite-builder': + default: { + const pkgJSON = getPkgJSON(cwd, 'vite') + const isRolldown = pkgJSON.name.includes('rolldown') + return { name: isRolldown ? 'Rolldown-Vite' : 'Vite', version: pkgJSON.version } } } +} - function getPkgJSON(pkg: string) { - for (const url of [cwd, tryResolveNuxt(cwd)]) { - if (!url) { - continue - } - const p = resolveModulePath(`${pkg}/package.json`, { from: url, try: true }) - if (p) { - return JSON.parse(readFileSync(p, 'utf-8')) - } - } - return null - } - - function getPkgVersion(pkg: string) { - const pkgJSON = getPkgJSON(pkg) - return pkgJSON?.version ?? '' - } +export function showVersionsFromConfig(cwd: string, config: NuxtOptions) { + const { bold, gray, green } = colors - const nuxtVersion = getPkgVersion('nuxt') || getPkgVersion('nuxt-nightly') || getPkgVersion('nuxt3') || getPkgVersion('nuxt-edge') - const nitroVersion = getPkgVersion('nitropack') || getPkgVersion('nitro') || getPkgVersion('nitropack-nightly') || getPkgVersion('nitropack-edge') - const builder = getBuilder() - const vueVersion = getPkgVersion('vue') || null + const nuxtVersion = getPkgVersion(cwd, 'nuxt') || getPkgVersion(cwd, 'nuxt-nightly') || getPkgVersion(cwd, 'nuxt3') || getPkgVersion(cwd, 'nuxt-edge') + const nitroVersion = getPkgVersion(cwd, 'nitropack') || getPkgVersion(cwd, 'nitro') || getPkgVersion(cwd, 'nitropack-nightly') || getPkgVersion(cwd, 'nitropack-edge') + const builder = getBuilder(cwd, config.builder) + const vueVersion = getPkgVersion(cwd, 'vue') || null - logger.log( + logger.info( green(`Nuxt ${bold(nuxtVersion)}`) + gray(' (with ') + (nitroVersion ? gray(`Nitro ${bold(nitroVersion)}`) : '') diff --git a/packages/nuxi/src/utils/console.ts b/packages/nuxi/src/utils/console.ts deleted file mode 100644 index 2d42928fd..000000000 --- a/packages/nuxi/src/utils/console.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { ConsolaReporter } from 'consola' - -import process from 'node:process' - -import { consola } from 'consola' - -// Filter out unwanted logs -// TODO: Use better API from consola for intercepting logs -function wrapReporter(reporter: ConsolaReporter) { - return ({ - log(logObj, ctx) { - if (!logObj.args || !logObj.args.length) { - return - } - const msg = logObj.args[0] - if (typeof msg === 'string' && !process.env.DEBUG) { - // Hide vue-router 404 warnings - if ( - msg.startsWith( - '[Vue Router warn]: No match found for location with path', - ) - ) { - return - } - // Suppress warning about native Node.js fetch - if ( - msg.includes( - 'ExperimentalWarning: The Fetch API is an experimental feature', - ) - ) { - return - } - // TODO: resolve upstream in Vite - // Hide sourcemap warnings related to node_modules - if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { - return - } - } - return reporter.log(logObj, ctx) - }, - }) satisfies ConsolaReporter -} - -export function setupGlobalConsole(opts: { dev?: boolean } = {}) { - consola.options.reporters = consola.options.reporters.map(wrapReporter) - - // Wrap all console logs with consola for better DX - if (opts.dev) { - consola.wrapAll() - } - else { - consola.wrapConsole() - } - - process.on('unhandledRejection', err => - consola.error('[unhandledRejection]', err)) - - process.on('uncaughtException', err => - consola.error('[uncaughtException]', err)) -} diff --git a/packages/nuxi/src/utils/fs.ts b/packages/nuxi/src/utils/fs.ts index d422241b7..ea83d4c37 100644 --- a/packages/nuxi/src/utils/fs.ts +++ b/packages/nuxi/src/utils/fs.ts @@ -1,7 +1,6 @@ import { existsSync, promises as fsp } from 'node:fs' import { join } from 'pathe' - -import { logger } from '../utils/logger' +import { debug } from '../utils/logger' export async function clearDir(path: string, exclude?: string[]) { if (!exclude) { @@ -29,7 +28,7 @@ export async function rmRecursive(paths: string[]) { paths .filter(p => typeof p === 'string') .map(async (path) => { - logger.debug('Removing recursive path', path) + debug(`Removing recursive path: ${path}`) await fsp.rm(path, { recursive: true, force: true }).catch(() => {}) }), ) diff --git a/packages/nuxi/src/utils/logger.ts b/packages/nuxi/src/utils/logger.ts index d0d9259e3..00eb78182 100644 --- a/packages/nuxi/src/utils/logger.ts +++ b/packages/nuxi/src/utils/logger.ts @@ -1,3 +1,5 @@ -import { consola } from 'consola' +import { log } from '@clack/prompts' +import createDebug from 'debug' -export const logger = consola.withTag('nuxi') +export const logger = log +export const debug = createDebug('nuxi') diff --git a/packages/nuxi/src/utils/paths.ts b/packages/nuxi/src/utils/paths.ts new file mode 100644 index 000000000..f20500b51 --- /dev/null +++ b/packages/nuxi/src/utils/paths.ts @@ -0,0 +1,8 @@ +import process from 'node:process' +import { relative } from 'pathe' + +const cwd = process.cwd() + +export function relativeToProcess(path: string) { + return relative(cwd, path) || path +} diff --git a/packages/nuxi/src/utils/versions.ts b/packages/nuxi/src/utils/versions.ts index 0f31a270d..d526ff548 100644 --- a/packages/nuxi/src/utils/versions.ts +++ b/packages/nuxi/src/utils/versions.ts @@ -1,6 +1,10 @@ +import { readFileSync } from 'node:fs' +import { resolveModulePath } from 'exsolve' import { readPackageJSON } from 'pkg-types' import { coerce } from 'semver' +import { tryResolveNuxt } from './kit' + export async function getNuxtVersion(cwd: string, cache = true) { const nuxtPkg = await readPackageJSON('nuxt', { url: cwd, try: true, cache }) if (nuxtPkg) { @@ -10,3 +14,21 @@ export async function getNuxtVersion(cwd: string, cache = true) { const pkgDep = pkg?.dependencies?.nuxt || pkg?.devDependencies?.nuxt return (pkgDep && coerce(pkgDep)?.version) || '3.0.0' } + +export function getPkgVersion(cwd: string, pkg: string) { + const pkgJSON = getPkgJSON(cwd, pkg) + return pkgJSON?.version ?? '' +} + +export function getPkgJSON(cwd: string, pkg: string) { + for (const url of [cwd, tryResolveNuxt(cwd)]) { + if (!url) { + continue + } + const p = resolveModulePath(`${pkg}/package.json`, { from: url, try: true }) + if (p) { + return JSON.parse(readFileSync(p, 'utf-8')) + } + } + return null +} diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index 2e9559b38..dda5e7d95 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -38,8 +38,8 @@ "c12": "^3.3.1", "citty": "^0.1.6", "confbox": "^0.2.2", - "consola": "^3.4.2", "copy-paste": "^2.2.0", + "debug": "^4.4.3", "defu": "^6.1.4", "exsolve": "^1.0.7", "fuse.js": "^7.1.0", @@ -51,6 +51,7 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", + "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "scule": "^1.3.0", "semver": "^7.7.3", @@ -63,6 +64,7 @@ "devDependencies": { "@nuxt/kit": "^4.2.0", "@nuxt/schema": "^4.2.0", + "@types/debug": "^4.1.12", "@types/node": "^24.10.0", "get-port-please": "^3.2.0", "h3": "^1.15.4", diff --git a/packages/nuxt-cli/src/main.ts b/packages/nuxt-cli/src/main.ts index 4ade7043f..254eb022e 100644 --- a/packages/nuxt-cli/src/main.ts +++ b/packages/nuxt-cli/src/main.ts @@ -7,7 +7,6 @@ import { provider } from 'std-env' import { commands } from '../../nuxi/src/commands' import { cwdArgs } from '../../nuxi/src/commands/_shared' -import { setupGlobalConsole } from '../../nuxi/src/utils/console' import { checkEngines } from '../../nuxi/src/utils/engines' import { logger } from '../../nuxi/src/utils/logger' @@ -29,8 +28,6 @@ const _main = defineCommand({ subCommands: commands, async setup(ctx) { const command = ctx.args._[0] - const dev = command === 'dev' - setupGlobalConsole({ dev }) // Check Node.js version and CLI updates in background let backgroundTasks: Promise | undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2b9f65e6..5f4fa2bbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: '@types/copy-paste': specifier: ^2.1.0 version: 2.1.0 + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 '@types/node': specifier: ^24.10.0 version: 24.10.0 @@ -149,12 +152,12 @@ importers: confbox: specifier: ^0.2.2 version: 0.2.2 - consola: - specifier: ^3.4.2 - version: 3.4.2 copy-paste: specifier: ^2.2.0 version: 2.2.0 + debug: + specifier: ^4.4.3 + version: 4.4.3 defu: specifier: ^6.1.4 version: 6.1.4 @@ -203,6 +206,9 @@ importers: perfect-debounce: specifier: ^2.0.0 version: 2.0.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 pkg-types: specifier: ^2.3.0 version: 2.3.0 @@ -263,12 +269,12 @@ importers: confbox: specifier: ^0.2.2 version: 0.2.2 - consola: - specifier: ^3.4.2 - version: 3.4.2 copy-paste: specifier: ^2.2.0 version: 2.2.0 + debug: + specifier: ^4.4.3 + version: 4.4.3 defu: specifier: ^6.1.4 version: 6.1.4 @@ -302,6 +308,9 @@ importers: perfect-debounce: specifier: ^2.0.0 version: 2.0.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 pkg-types: specifier: ^2.3.0 version: 2.3.0 @@ -333,6 +342,9 @@ importers: '@nuxt/schema': specifier: 4.2.0 version: 4.2.0 + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 '@types/node': specifier: ^24.10.0 version: 24.10.0 From b2e575cf4133934bf81810178692b62760fb7dc8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 15:22:04 +0000 Subject: [PATCH 02/11] chore: update knip --- knip.json | 1 + 1 file changed, 1 insertion(+) diff --git a/knip.json b/knip.json index 27d4d3858..16ad4c0ff 100644 --- a/knip.json +++ b/knip.json @@ -47,6 +47,7 @@ "ohash", "pathe", "perfect-debounce", + "picocolors", "pkg-types", "scule", "semver", From b693ad588b3186bc9d9c9598bf1000e41f143efd Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 15:31:54 +0000 Subject: [PATCH 03/11] fix: wrap more things in cyan --- packages/nuxi/src/commands/dev.ts | 3 ++- packages/nuxi/src/commands/devtools.ts | 3 ++- packages/nuxi/src/commands/init.ts | 4 +-- packages/nuxi/src/commands/module/add.ts | 31 ++++++++++++------------ packages/nuxi/src/commands/preview.ts | 4 +-- packages/nuxi/src/commands/test.ts | 2 +- packages/nuxi/src/utils/engines.ts | 3 ++- packages/nuxi/src/utils/env.ts | 3 ++- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index e9ef9b786..31f439d81 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -6,6 +6,7 @@ import process from 'node:process' import { defineCommand } from 'citty' import { getArgs as getListhenArgs } from 'listhen/cli' import { resolve } from 'pathe' +import colors from 'picocolors' import { satisfies } from 'semver' import { isBun, isTest } from 'std-env' @@ -132,7 +133,7 @@ const command = defineCommand({ void restartWithFork() } else if (message.type === 'nuxt:internal:dev:rejection') { - logger.info(`Restarting Nuxt due to error: \`${message.message}\``) + logger.info(`Restarting Nuxt due to error: ${colors.cyan(message.message)}`) void restartWithFork() } }) diff --git a/packages/nuxi/src/commands/devtools.ts b/packages/nuxi/src/commands/devtools.ts index 37cc61709..b425285d3 100644 --- a/packages/nuxi/src/commands/devtools.ts +++ b/packages/nuxi/src/commands/devtools.ts @@ -2,6 +2,7 @@ import process from 'node:process' import { defineCommand } from 'citty' import { resolve } from 'pathe' +import colors from 'picocolors' import { x } from 'tinyexec' import { logger } from '../utils/logger' @@ -25,7 +26,7 @@ export default defineCommand({ const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) if (!['enable', 'disable'].includes(ctx.args.command)) { - logger.error(`Unknown command \`${ctx.args.command}\`.`) + logger.error(`Unknown command ${colors.cyan(ctx.args.command)}.`) process.exit(1) } diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 658e015d5..cecdd1b44 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -43,7 +43,7 @@ async function getModuleDependencies(moduleName: string) { return Object.keys(dependencies) } catch (err) { - logger.warn(`Could not get dependencies for ${moduleName}: ${err}`) + logger.warn(`Could not get dependencies for ${colors.cyan(moduleName)}: ${err}`) return [] } } @@ -301,7 +301,7 @@ export default defineCommand({ const nightlyChannelVersion = response['dist-tags'][nightlyChannelTag] if (!nightlyChannelVersion) { - logger.error(`Nightly channel version for tag '${nightlyChannelTag}' not found.`) + logger.error(`Nightly channel version for tag ${colors.cyan(nightlyChannelTag)} not found.`) process.exit(1) } diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index 08391dfca..22476f460 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -21,6 +21,7 @@ import { joinURL } from 'ufo' import { runCommand } from '../../run' import { logger } from '../../utils/logger' +import { relativeToProcess } from '../../utils/paths' import { getNuxtVersion } from '../../utils/versions' import { cwdArgs, logLevelArgs } from '../_shared' import prepareCommand from '../prepare' @@ -71,7 +72,7 @@ export default defineCommand({ const projectPkg = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) if (!projectPkg.dependencies?.nuxt && !projectPkg.devDependencies?.nuxt) { - logger.warn(`No \`nuxt\` dependency detected in \`${cwd}\`.`) + logger.warn(`No ${colors.cyan('nuxt')} dependency detected in ${colors.cyan(relativeToProcess(cwd))}.`) const shouldContinue = await confirm({ message: `Do you want to continue anyway?`, @@ -86,7 +87,7 @@ export default defineCommand({ const maybeResolvedModules = await Promise.all(modules.map(moduleName => resolveModule(moduleName, cwd))) const resolvedModules = maybeResolvedModules.filter((x: ModuleResolution): x is ResolvedModule => x != null) - logger.info(`Resolved \`${resolvedModules.map(x => x.pkgName).join('\`, \`')}\`, adding module${resolvedModules.length > 1 ? 's' : ''}...`) + logger.info(`Resolved ${resolvedModules.map(x => colors.cyan(x.pkgName)).join(', ')}, adding module${resolvedModules.length > 1 ? 's' : ''}...`) await addModules(resolvedModules, { ...ctx.args, cwd }, projectPkg) @@ -121,18 +122,18 @@ async function addModules(modules: ResolvedModule[], { skipInstall, skipConfig, } if (installedModules.length > 0) { - const installedModulesList = installedModules.map(module => module.pkgName).join('\`, \`') + const installedModulesList = installedModules.map(module => colors.cyan(module.pkgName)).join(', ') const are = installedModules.length > 1 ? 'are' : 'is' - logger.info(`\`${installedModulesList}\` ${are} already installed`) + logger.info(`${installedModulesList} ${are} already installed`) } if (notInstalledModules.length > 0) { const isDev = Boolean(projectPkg.devDependencies?.nuxt) || dev - const notInstalledModulesList = notInstalledModules.map(module => module.pkg).join('\`, \`') + const notInstalledModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join(', ') const dependency = notInstalledModules.length > 1 ? 'dependencies' : 'dependency' const a = notInstalledModules.length > 1 ? '' : ' a' - logger.info(`Installing \`${notInstalledModulesList}\` as${a}${isDev ? ' development' : ''} ${dependency}`) + logger.info(`Installing ${notInstalledModulesList} as${a}${isDev ? ' development' : ''} ${dependency}`) const packageManager = await detectPackageManager(cwd) @@ -173,7 +174,7 @@ async function addModules(modules: ResolvedModule[], { skipInstall, skipConfig, cwd, configFile: 'nuxt.config', async onCreate() { - logger.info(`Creating \`nuxt.config.ts\``) + logger.info(`Creating ${colors.cyan('nuxt.config.ts')}`) return getDefaultNuxtConfig() }, @@ -184,19 +185,19 @@ async function addModules(modules: ResolvedModule[], { skipInstall, skipConfig, for (const resolved of modules) { if (config.modules.includes(resolved.pkgName)) { - logger.info(`\`${resolved.pkgName}\` is already in the \`modules\``) + logger.info(`${colors.cyan(resolved.pkgName)} is already in the ${colors.cyan('modules')}`) continue } - logger.info(`Adding \`${resolved.pkgName}\` to the \`modules\``) + logger.info(`Adding ${colors.cyan(resolved.pkgName)} to the ${colors.cyan('modules')}`) config.modules.push(resolved.pkgName) } }, }).catch((error) => { - logger.error(`Failed to update \`nuxt.config\`: ${error.message}`) - logger.error(`Please manually add \`${modules.map(module => module.pkgName).join('\`, \`')}\` to the \`modules\` in \`nuxt.config.ts\``) + logger.error(`Failed to update ${colors.cyan('nuxt.config')}: ${error.message}`) + logger.error(`Please manually add ${colors.cyan(modules.map(module => module.pkgName).join(', '))} to the ${colors.cyan('modules')} in ${colors.cyan('nuxt.config.ts')}`) return null }) @@ -227,7 +228,7 @@ async function resolveModule(moduleName: string, cwd: string): Promise { err = _err } } - logger.error(err) + logger.error(err as string) throw new Error('`@nuxt/test-utils` seems missing. Run `npm i -D @nuxt/test-utils` or `yarn add -D @nuxt/test-utils` to install.') } diff --git a/packages/nuxi/src/utils/engines.ts b/packages/nuxi/src/utils/engines.ts index 5558efc4e..f913e0a3c 100644 --- a/packages/nuxi/src/utils/engines.ts +++ b/packages/nuxi/src/utils/engines.ts @@ -1,4 +1,5 @@ import process from 'node:process' +import colors from 'picocolors' import { logger } from './logger' @@ -12,7 +13,7 @@ export async function checkEngines() { if (!satisfies(currentNode, nodeRange)) { logger.warn( - `Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version \`${nodeRange}\`.`, + `Current version of Node.js (${colors.cyan(currentNode)}) is unsupported and might cause issues.\n Please upgrade to a compatible version ${colors.cyan(nodeRange)}.`, ) } } diff --git a/packages/nuxi/src/utils/env.ts b/packages/nuxi/src/utils/env.ts index b5bec0027..f4db359ab 100644 --- a/packages/nuxi/src/utils/env.ts +++ b/packages/nuxi/src/utils/env.ts @@ -1,4 +1,5 @@ import process from 'node:process' +import colors from 'picocolors' import { logger } from './logger' @@ -6,7 +7,7 @@ export function overrideEnv(targetEnv: string) { const currentEnv = process.env.NODE_ENV if (currentEnv && currentEnv !== targetEnv) { logger.warn( - `Changing \`NODE_ENV\` from \`${currentEnv}\` to \`${targetEnv}\`, to avoid unintended behavior.`, + `Changing ${colors.cyan('NODE_ENV')} from ${colors.cyan(currentEnv)} to ${colors.cyan(targetEnv)}, to avoid unintended behavior.`, ) } From b938d0bc5da3b403683c85cdd4441120664815e7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 16:00:20 +0000 Subject: [PATCH 04/11] fix: add back global console + consola colors --- knip.json | 2 +- packages/nuxi/package.json | 2 +- packages/nuxi/src/commands/add.ts | 2 +- packages/nuxi/src/commands/analyze.ts | 2 +- packages/nuxi/src/commands/build.ts | 2 +- packages/nuxi/src/commands/dev.ts | 2 +- packages/nuxi/src/commands/devtools.ts | 2 +- packages/nuxi/src/commands/info.ts | 2 +- packages/nuxi/src/commands/init.ts | 2 +- packages/nuxi/src/commands/module/add.ts | 2 +- packages/nuxi/src/commands/module/search.ts | 2 +- packages/nuxi/src/commands/prepare.ts | 2 +- packages/nuxi/src/commands/preview.ts | 2 +- packages/nuxi/src/commands/upgrade.ts | 2 +- packages/nuxi/src/main.ts | 2 + packages/nuxi/src/utils/banner.ts | 2 +- packages/nuxi/src/utils/console.ts | 60 +++++++++++++++++++++ packages/nuxi/src/utils/engines.ts | 2 +- packages/nuxi/src/utils/env.ts | 2 +- packages/nuxt-cli/package.json | 2 +- packages/nuxt-cli/src/main.ts | 4 +- pnpm-lock.yaml | 16 +++--- 22 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 packages/nuxi/src/utils/console.ts diff --git a/knip.json b/knip.json index 16ad4c0ff..3a90819d6 100644 --- a/knip.json +++ b/knip.json @@ -32,6 +32,7 @@ "@clack/prompts", "c12", "confbox", + "consola", "copy-paste", "debug", "defu", @@ -47,7 +48,6 @@ "ohash", "pathe", "perfect-debounce", - "picocolors", "pkg-types", "scule", "semver", diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index 573838639..ae7bd4486 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -44,6 +44,7 @@ "c12": "^3.3.1", "citty": "^0.1.6", "confbox": "^0.2.2", + "consola": "^3.4.2", "copy-paste": "^2.2.0", "debug": "^4.4.3", "defu": "^6.1.4", @@ -62,7 +63,6 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", - "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "rollup": "^4.52.5", "rollup-plugin-visualizer": "^6.0.5", diff --git a/packages/nuxi/src/commands/add.ts b/packages/nuxi/src/commands/add.ts index 78140aa65..215d70114 100644 --- a/packages/nuxi/src/commands/add.ts +++ b/packages/nuxi/src/commands/add.ts @@ -3,8 +3,8 @@ import process from 'node:process' import { cancel, intro, outro } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { dirname, extname, resolve } from 'pathe' -import colors from 'picocolors' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts index 4f9f6b825..5f5c8db99 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -5,10 +5,10 @@ import process from 'node:process' import { intro, note, outro, taskLog } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { defu } from 'defu' import { H3, lazyEventHandler } from 'h3-next' import { join, resolve } from 'pathe' -import colors from 'picocolors' import { serve } from 'srvx' import { overrideEnv } from '../utils/env' diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index c9c32cd87..71ca2b12b 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -4,8 +4,8 @@ import process from 'node:process' import { intro, outro } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { relative, resolve } from 'pathe' -import colors from 'picocolors' import { showVersions } from '../utils/banner' import { overrideEnv } from '../utils/env' diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 31f439d81..1eedcade4 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -4,9 +4,9 @@ import type { NuxtDevContext } from '../dev/utils' import process from 'node:process' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { getArgs as getListhenArgs } from 'listhen/cli' import { resolve } from 'pathe' -import colors from 'picocolors' import { satisfies } from 'semver' import { isBun, isTest } from 'std-env' diff --git a/packages/nuxi/src/commands/devtools.ts b/packages/nuxi/src/commands/devtools.ts index b425285d3..3ca1b5e7d 100644 --- a/packages/nuxi/src/commands/devtools.ts +++ b/packages/nuxi/src/commands/devtools.ts @@ -1,8 +1,8 @@ import process from 'node:process' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { resolve } from 'pathe' -import colors from 'picocolors' import { x } from 'tinyexec' import { logger } from '../utils/logger' diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts index f048f0b17..c3e692dbb 100644 --- a/packages/nuxi/src/commands/info.ts +++ b/packages/nuxi/src/commands/info.ts @@ -7,10 +7,10 @@ import { stripVTControlCharacters } from 'node:util' import { box } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { copy as copyToClipboard } from 'copy-paste' import { detectPackageManager } from 'nypm' import { resolve } from 'pathe' -import colors from 'picocolors' import { readPackageJSON } from 'pkg-types' import { isBun, isDeno, isMinimal } from 'std-env' diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index cecdd1b44..955851535 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -6,11 +6,11 @@ import process from 'node:process' import { box, cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { downloadTemplate, startShell } from 'giget' import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' import { basename, join, relative, resolve } from 'pathe' -import colors from 'picocolors' import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' import { hasTTY } from 'std-env' diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index 22476f460..d3d22455f 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -11,10 +11,10 @@ import process from 'node:process' import { confirm, isCancel, select } from '@clack/prompts' import { updateConfig } from 'c12/update' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { addDependency, detectPackageManager } from 'nypm' import { $fetch } from 'ofetch' import { resolve } from 'pathe' -import colors from 'picocolors' import { readPackageJSON } from 'pkg-types' import { satisfies } from 'semver' import { joinURL } from 'ufo' diff --git a/packages/nuxi/src/commands/module/search.ts b/packages/nuxi/src/commands/module/search.ts index b8be09b78..c9f7190eb 100644 --- a/packages/nuxi/src/commands/module/search.ts +++ b/packages/nuxi/src/commands/module/search.ts @@ -1,6 +1,6 @@ import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import Fuse from 'fuse.js' -import colors from 'picocolors' import { kebabCase, upperFirst } from 'scule' import { logger } from '../../utils/logger' diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index d9f4c42c4..ed1cff5ba 100644 --- a/packages/nuxi/src/commands/prepare.ts +++ b/packages/nuxi/src/commands/prepare.ts @@ -1,8 +1,8 @@ import process from 'node:process' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { resolve } from 'pathe' -import colors from 'picocolors' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' diff --git a/packages/nuxi/src/commands/preview.ts b/packages/nuxi/src/commands/preview.ts index 27fd97caa..1ed966d79 100644 --- a/packages/nuxi/src/commands/preview.ts +++ b/packages/nuxi/src/commands/preview.ts @@ -5,8 +5,8 @@ import process from 'node:process' import { box, outro } from '@clack/prompts' import { setupDotenv } from 'c12' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { resolve } from 'pathe' -import colors from 'picocolors' import { x } from 'tinyexec' import { loadKit } from '../utils/kit' diff --git a/packages/nuxi/src/commands/upgrade.ts b/packages/nuxi/src/commands/upgrade.ts index 64dc38bcb..71fee65d7 100644 --- a/packages/nuxi/src/commands/upgrade.ts +++ b/packages/nuxi/src/commands/upgrade.ts @@ -5,9 +5,9 @@ import process from 'node:process' import { cancel, intro, isCancel, note, outro, select, spinner, tasks } from '@clack/prompts' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { addDependency, dedupeDependencies, detectPackageManager } from 'nypm' import { resolve } from 'pathe' -import colors from 'picocolors' import { findWorkspaceDir, readPackageJSON } from 'pkg-types' import { loadKit } from '../utils/kit' diff --git a/packages/nuxi/src/main.ts b/packages/nuxi/src/main.ts index 5ded34fdf..1d0b631a2 100644 --- a/packages/nuxi/src/main.ts +++ b/packages/nuxi/src/main.ts @@ -13,6 +13,7 @@ import { cwdArgs } from './commands/_shared' import { initCompletions } from './completions' import { checkEngines } from './utils/engines' import { debug, logger } from './utils/logger' +import { setupGlobalConsole } from './utils/console' // globalThis.crypto support for Node.js 18 if (!globalThis.crypto) { @@ -46,6 +47,7 @@ const _main = defineCommand({ subCommands: commands, async setup(ctx) { const command = ctx.args._[0] + setupGlobalConsole({ dev: command === 'dev' }) debug(`Running \`nuxt ${command}\` command`) // Check Node.js version and CLI updates in background diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 4c8f24fcf..980746235 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -1,6 +1,6 @@ import type { NuxtBuilder, NuxtConfig, NuxtOptions } from '@nuxt/schema' -import colors from 'picocolors' +import { colors } from 'consola/utils' import { logger } from './logger' import { getPkgJSON, getPkgVersion } from './versions' diff --git a/packages/nuxi/src/utils/console.ts b/packages/nuxi/src/utils/console.ts new file mode 100644 index 000000000..2d42928fd --- /dev/null +++ b/packages/nuxi/src/utils/console.ts @@ -0,0 +1,60 @@ +import type { ConsolaReporter } from 'consola' + +import process from 'node:process' + +import { consola } from 'consola' + +// Filter out unwanted logs +// TODO: Use better API from consola for intercepting logs +function wrapReporter(reporter: ConsolaReporter) { + return ({ + log(logObj, ctx) { + if (!logObj.args || !logObj.args.length) { + return + } + const msg = logObj.args[0] + if (typeof msg === 'string' && !process.env.DEBUG) { + // Hide vue-router 404 warnings + if ( + msg.startsWith( + '[Vue Router warn]: No match found for location with path', + ) + ) { + return + } + // Suppress warning about native Node.js fetch + if ( + msg.includes( + 'ExperimentalWarning: The Fetch API is an experimental feature', + ) + ) { + return + } + // TODO: resolve upstream in Vite + // Hide sourcemap warnings related to node_modules + if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { + return + } + } + return reporter.log(logObj, ctx) + }, + }) satisfies ConsolaReporter +} + +export function setupGlobalConsole(opts: { dev?: boolean } = {}) { + consola.options.reporters = consola.options.reporters.map(wrapReporter) + + // Wrap all console logs with consola for better DX + if (opts.dev) { + consola.wrapAll() + } + else { + consola.wrapConsole() + } + + process.on('unhandledRejection', err => + consola.error('[unhandledRejection]', err)) + + process.on('uncaughtException', err => + consola.error('[uncaughtException]', err)) +} diff --git a/packages/nuxi/src/utils/engines.ts b/packages/nuxi/src/utils/engines.ts index f913e0a3c..167c7d82e 100644 --- a/packages/nuxi/src/utils/engines.ts +++ b/packages/nuxi/src/utils/engines.ts @@ -1,5 +1,5 @@ import process from 'node:process' -import colors from 'picocolors' +import { colors } from 'consola/utils' import { logger } from './logger' diff --git a/packages/nuxi/src/utils/env.ts b/packages/nuxi/src/utils/env.ts index f4db359ab..7f9e7ea97 100644 --- a/packages/nuxi/src/utils/env.ts +++ b/packages/nuxi/src/utils/env.ts @@ -1,5 +1,5 @@ import process from 'node:process' -import colors from 'picocolors' +import { colors } from 'consola/utils' import { logger } from './logger' diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index dda5e7d95..2f6560fec 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -38,6 +38,7 @@ "c12": "^3.3.1", "citty": "^0.1.6", "confbox": "^0.2.2", + "consola": "^3.4.2", "copy-paste": "^2.2.0", "debug": "^4.4.3", "defu": "^6.1.4", @@ -51,7 +52,6 @@ "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", - "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "scule": "^1.3.0", "semver": "^7.7.3", diff --git a/packages/nuxt-cli/src/main.ts b/packages/nuxt-cli/src/main.ts index 254eb022e..8bddb2601 100644 --- a/packages/nuxt-cli/src/main.ts +++ b/packages/nuxt-cli/src/main.ts @@ -7,9 +7,10 @@ import { provider } from 'std-env' import { commands } from '../../nuxi/src/commands' import { cwdArgs } from '../../nuxi/src/commands/_shared' +import { setupGlobalConsole } from '../../nuxi/src/utils/console' import { checkEngines } from '../../nuxi/src/utils/engines' -import { logger } from '../../nuxi/src/utils/logger' +import { logger } from '../../nuxi/src/utils/logger' import { description, name, version } from '../package.json' const _main = defineCommand({ @@ -28,6 +29,7 @@ const _main = defineCommand({ subCommands: commands, async setup(ctx) { const command = ctx.args._[0] + setupGlobalConsole({ dev: command === 'dev' }) // Check Node.js version and CLI updates in background let backgroundTasks: Promise | undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f4fa2bbd..5afdcd431 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: confbox: specifier: ^0.2.2 version: 0.2.2 + consola: + specifier: ^3.4.2 + version: 3.4.2 copy-paste: specifier: ^2.2.0 version: 2.2.0 @@ -206,9 +209,6 @@ importers: perfect-debounce: specifier: ^2.0.0 version: 2.0.0 - picocolors: - specifier: ^1.1.1 - version: 1.1.1 pkg-types: specifier: ^2.3.0 version: 2.3.0 @@ -262,13 +262,16 @@ importers: version: 1.0.0-alpha.6 c12: specifier: ^3.3.1 - version: 3.3.1(magicast@0.3.5) + version: 3.3.1(magicast@0.5.1) citty: specifier: ^0.1.6 version: 0.1.6 confbox: specifier: ^0.2.2 version: 0.2.2 + consola: + specifier: ^3.4.2 + version: 3.4.2 copy-paste: specifier: ^2.2.0 version: 2.2.0 @@ -308,9 +311,6 @@ importers: perfect-debounce: specifier: ^2.0.0 version: 2.0.0 - picocolors: - specifier: ^1.1.1 - version: 1.1.1 pkg-types: specifier: ^2.3.0 version: 2.3.0 @@ -338,7 +338,7 @@ importers: devDependencies: '@nuxt/kit': specifier: ^4.2.0 - version: 4.2.0(magicast@0.3.5) + version: 4.2.0(magicast@0.5.1) '@nuxt/schema': specifier: 4.2.0 version: 4.2.0 From 590b407caf04c5a4fc0835915e87fde5aa108206 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:01:07 +0000 Subject: [PATCH 05/11] [autofix.ci] apply automated fixes --- packages/nuxi/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxi/src/main.ts b/packages/nuxi/src/main.ts index 1d0b631a2..e6f7fc595 100644 --- a/packages/nuxi/src/main.ts +++ b/packages/nuxi/src/main.ts @@ -11,9 +11,9 @@ import { description, name, version } from '../package.json' import { commands } from './commands' import { cwdArgs } from './commands/_shared' import { initCompletions } from './completions' +import { setupGlobalConsole } from './utils/console' import { checkEngines } from './utils/engines' import { debug, logger } from './utils/logger' -import { setupGlobalConsole } from './utils/console' // globalThis.crypto support for Node.js 18 if (!globalThis.crypto) { From f3d695689c2357a81d7358cd8536943f3a95c1ad Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 16:07:16 +0000 Subject: [PATCH 06/11] feat(info): use formatted markdown table --- packages/nuxi/src/commands/info.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts index c3e692dbb..58609f0d4 100644 --- a/packages/nuxi/src/commands/info.ts +++ b/packages/nuxi/src/commands/info.ts @@ -122,12 +122,16 @@ export default defineCommand({ logger.info(`Nuxt root directory: ${colors.cyan(nuxtConfig.rootDir || cwd)}\n`) let firstColumnLength = 0 + let secondColumnLength = 0 let ansiFirstColumnLength = 0 const entries = Object.entries(infoObj).map(([label, val]) => { if (label.length > firstColumnLength) { ansiFirstColumnLength = colors.bold(colors.whiteBright(label)).length + 4 firstColumnLength = label.length + 4 } + if ((val || '').length > secondColumnLength) { + secondColumnLength = (val || '').length + 2 + } return [label, val || '-'] as const }) @@ -135,11 +139,11 @@ export default defineCommand({ const terminalWidth = Math.max(process.stdout.columns || 80, firstColumnLength) - 4 /* box padding */ // formatted for copy-pasting into an issue - let copyStr = '| | |\n| --- | --- |\n' + let copyStr = `| ${' '.repeat(firstColumnLength)} | ${' '.repeat(secondColumnLength)} |\n| ${'-'.repeat(firstColumnLength)} | ${'-'.repeat(secondColumnLength)} |\n` let boxStr = '' for (const [label, value] of entries) { if (!isMinimal) { - copyStr += `| **${label}** | ${value.includes('`') ? value : `\`${value}\``} |\n` + copyStr += `| ${`**${label}**`.padEnd(firstColumnLength)} | ${(value.includes('`') ? value : `\`${value}\``).padEnd(secondColumnLength)} |\n` } const formattedValue = value .replace(/\b@([^, ]+)/g, (_, r) => colors.gray(` ${r}`)) From fe2deb38e8019128486aecd84686725a80ec181f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 16:15:34 +0000 Subject: [PATCH 07/11] fix(init): use relative path --- packages/nuxi/src/commands/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 955851535..fa5cff64b 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -204,7 +204,7 @@ export default defineCommand({ const shouldVerify = !shouldForce && existsSync(templateDownloadPath) if (shouldVerify) { const selectedAction = await select({ - message: `The directory ${colors.cyan(templateDownloadPath)} already exists. What would you like to do?`, + message: `The directory ${colors.cyan(relativeToProcess(templateDownloadPath))} already exists. What would you like to do?`, options: [ { value: 'override', label: 'Override its contents' }, { value: 'different', label: 'Select different directory' }, From 1a8bd30a5d329538995ef91d8540ccf27d1403e8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Nov 2025 16:19:48 +0000 Subject: [PATCH 08/11] chore: update box wording --- packages/nuxi/src/commands/analyze.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts index 5f5c8db99..d76f59cf8 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -137,7 +137,7 @@ export default defineCommand({ await nuxt.callHook('build:analyze:done', meta) await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8') - note(`${relativeToProcess(analyzeDir)}\n\nDo not deploy analyze results! Use ${colors.cyan('nuxt build')} before deploying.`, 'Analyze results') + note(`${relativeToProcess(analyzeDir)}\n\nDo not deploy analyze results! Use ${colors.cyan('nuxt build')} before deploying.`, 'Build location') if (ctx.args.serve !== false && !process.env.CI) { const app = new H3() @@ -157,7 +157,7 @@ export default defineCommand({ await serve(app).serve() } else { - outro('✨ Analysis complete!') + outro('✨ Analysis build complete!') } }, }) From 5216eeed6ce22fc04a5f3af76ea083432a7a3f7c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 7 Nov 2025 12:40:51 +0000 Subject: [PATCH 09/11] fix: remove some backticks --- packages/nuxi/src/commands/init.ts | 2 +- packages/nuxi/src/commands/module/add.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index fa5cff64b..86f280508 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -515,7 +515,7 @@ export default defineCommand({ await runCommand(addModuleCommand, args) } - outro(`✨ Nuxt project has been created with the \`${template.name}\` template.`) + outro(`✨ Nuxt project has been created with the ${colors.cyan(template.name)} template.`) // Display next steps const relativeTemplateDir = relative(process.cwd(), template.dir) || '.' diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index d3d22455f..34b8df67a 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -147,10 +147,10 @@ async function addModules(modules: ResolvedModule[], { skipInstall, skipConfig, async (error) => { logger.error(error) - const failedModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join('\`, \`') + const failedModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join(', ') const s = notInstalledModules.length > 1 ? 's' : '' const result = await confirm({ - message: `Install failed for \`${failedModulesList}\`. Do you want to continue adding the module${s} to ${colors.cyan('nuxt.config')}?`, + message: `Install failed for ${failedModulesList}. Do you want to continue adding the module${s} to ${colors.cyan('nuxt.config')}?`, initialValue: false, }) From 5b19faf8916244d65b97d84bbcd949deb0c3a260 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 14 Nov 2025 10:56:49 +0000 Subject: [PATCH 10/11] chore: pin clack version --- packages/nuxi/package.json | 2 +- packages/nuxt-cli/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index ae7bd4486..87fe2c482 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -33,7 +33,7 @@ }, "devDependencies": { "@bomb.sh/tab": "^0.0.9", - "@clack/prompts": "^1.0.0-alpha.6", + "@clack/prompts": "1.0.0-alpha.6", "@nuxt/kit": "^4.2.0", "@nuxt/schema": "^4.2.0", "@nuxt/test-utils": "^3.20.1", diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index 2f6560fec..0ce5e7114 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@bomb.sh/tab": "^0.0.9", - "@clack/prompts": "^1.0.0-alpha.6", + "@clack/prompts": "1.0.0-alpha.6", "c12": "^3.3.1", "citty": "^0.1.6", "confbox": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5afdcd431..e2b84704c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,7 +120,7 @@ importers: specifier: ^0.0.9 version: 0.0.9(cac@6.7.14)(citty@0.1.6) '@clack/prompts': - specifier: ^1.0.0-alpha.6 + specifier: 1.0.0-alpha.6 version: 1.0.0-alpha.6 '@nuxt/kit': specifier: ^4.2.0 @@ -258,11 +258,11 @@ importers: specifier: ^0.0.9 version: 0.0.9(cac@6.7.14)(citty@0.1.6) '@clack/prompts': - specifier: ^1.0.0-alpha.6 + specifier: 1.0.0-alpha.6 version: 1.0.0-alpha.6 c12: specifier: ^3.3.1 - version: 3.3.1(magicast@0.5.1) + version: 3.3.1(magicast@0.3.5) citty: specifier: ^0.1.6 version: 0.1.6 @@ -338,7 +338,7 @@ importers: devDependencies: '@nuxt/kit': specifier: ^4.2.0 - version: 4.2.0(magicast@0.5.1) + version: 4.2.0(magicast@0.3.5) '@nuxt/schema': specifier: 4.2.0 version: 4.2.0 From 567b2f2f6a7d6cb37676252b26a53cd25dddf18e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 14 Nov 2025 16:59:27 +0000 Subject: [PATCH 11/11] feat: use cyan for cyan box + use shared util for module search --- packages/nuxi/src/commands/info.ts | 27 +----- packages/nuxi/src/commands/module/search.ts | 57 ++++++------ packages/nuxi/src/utils/formatting.ts | 96 +++++++++++++++++++++ 3 files changed, 129 insertions(+), 51 deletions(-) create mode 100644 packages/nuxi/src/utils/formatting.ts diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts index 58609f0d4..b29b59070 100644 --- a/packages/nuxi/src/commands/info.ts +++ b/packages/nuxi/src/commands/info.ts @@ -3,7 +3,6 @@ import type { PackageJson } from 'pkg-types' import os from 'node:os' import process from 'node:process' -import { stripVTControlCharacters } from 'node:util' import { box } from '@clack/prompts' import { defineCommand } from 'citty' @@ -17,6 +16,7 @@ import { isBun, isDeno, isMinimal } from 'std-env' import { version as nuxiVersion } from '../../package.json' import { getBuilder } from '../utils/banner' +import { formatInfoBox } from '../utils/formatting' import { tryResolveNuxt } from '../utils/kit' import { logger } from '../utils/logger' import { getPackageManagerVersion } from '../utils/packageManagers' @@ -121,12 +121,12 @@ export default defineCommand({ logger.info(`Nuxt root directory: ${colors.cyan(nuxtConfig.rootDir || cwd)}\n`) + const boxStr = formatInfoBox(infoObj) + let firstColumnLength = 0 let secondColumnLength = 0 - let ansiFirstColumnLength = 0 const entries = Object.entries(infoObj).map(([label, val]) => { if (label.length > firstColumnLength) { - ansiFirstColumnLength = colors.bold(colors.whiteBright(label)).length + 4 firstColumnLength = label.length + 4 } if ((val || '').length > secondColumnLength) { @@ -135,33 +135,12 @@ export default defineCommand({ return [label, val || '-'] as const }) - // get maximum width of terminal - const terminalWidth = Math.max(process.stdout.columns || 80, firstColumnLength) - 4 /* box padding */ - // formatted for copy-pasting into an issue let copyStr = `| ${' '.repeat(firstColumnLength)} | ${' '.repeat(secondColumnLength)} |\n| ${'-'.repeat(firstColumnLength)} | ${'-'.repeat(secondColumnLength)} |\n` - let boxStr = '' for (const [label, value] of entries) { if (!isMinimal) { copyStr += `| ${`**${label}**`.padEnd(firstColumnLength)} | ${(value.includes('`') ? value : `\`${value}\``).padEnd(secondColumnLength)} |\n` } - const formattedValue = value - .replace(/\b@([^, ]+)/g, (_, r) => colors.gray(` ${r}`)) - .replace(/`([^`]*)`/g, (_, r) => r) - - boxStr += (`${colors.bold(colors.whiteBright(label))}: `).padEnd(ansiFirstColumnLength) - - let boxRowLength = firstColumnLength - for (const item of formattedValue.split(', ')) { - const itemLength = stripVTControlCharacters(item).length + 2 - if (boxRowLength + itemLength > terminalWidth) { - boxStr += `\n${' '.repeat(firstColumnLength)}` - boxRowLength = firstColumnLength - } - boxStr += `${item}, ` - boxRowLength += itemLength - } - boxStr = `${boxStr.slice(0, -2)}\n` } const copied = !isMinimal && await new Promise(resolve => copyToClipboard(copyStr, err => resolve(!err))) diff --git a/packages/nuxi/src/commands/module/search.ts b/packages/nuxi/src/commands/module/search.ts index c9f7190eb..d583376d1 100644 --- a/packages/nuxi/src/commands/module/search.ts +++ b/packages/nuxi/src/commands/module/search.ts @@ -1,8 +1,10 @@ +import { box } from '@clack/prompts' import { defineCommand } from 'citty' import { colors } from 'consola/utils' import Fuse from 'fuse.js' import { kebabCase, upperFirst } from 'scule' +import { formatInfoBox } from '../../utils/formatting' import { logger } from '../../utils/logger' import { getNuxtVersion } from '../../utils/versions' import { cwdArgs } from '../_shared' @@ -58,19 +60,17 @@ async function findModuleByKeywords(query: string, nuxtVersion: string) { ], }) - const { bold, green, magenta, cyan, gray, yellow } = colors - const results = fuse.search(query).map((result) => { const res: Record = { - name: bold(result.item.name), - homepage: cyan(result.item.website), + name: result.item.name, + package: result.item.npm, + homepage: colors.cyan(result.item.website), compatibility: `nuxt: ${result.item.compatibility?.nuxt || '*'}`, - repository: gray(result.item.github), - description: gray(result.item.description), - package: gray(result.item.npm), - install: cyan(`npx nuxt module add ${result.item.name}`), - stars: yellow(formatNumber(result.item.stats.stars)), - monthlyDownloads: yellow(formatNumber(result.item.stats.downloads)), + repository: result.item.github, + description: result.item.description, + install: `npx nuxt module add ${result.item.name}`, + stars: colors.yellow(formatNumber(result.item.stats.stars)), + monthlyDownloads: colors.yellow(formatNumber(result.item.stats.downloads)), } if (result.item.github === result.item.website) { delete res.homepage @@ -83,31 +83,34 @@ async function findModuleByKeywords(query: string, nuxtVersion: string) { if (!results.length) { logger.info( - `No Nuxt modules found matching query ${magenta(query)} for Nuxt ${cyan(nuxtVersion)}`, + `No Nuxt modules found matching query ${colors.magenta(query)} for Nuxt ${colors.cyan(nuxtVersion)}`, ) return } logger.success( - `Found ${results.length} Nuxt ${results.length > 1 ? 'modules' : 'module'} matching ${cyan(query)} ${nuxtVersion ? `for Nuxt ${cyan(nuxtVersion)}` : ''}:\n`, + `Found ${results.length} Nuxt ${results.length > 1 ? 'modules' : 'module'} matching ${colors.cyan(query)} ${nuxtVersion ? `for Nuxt ${colors.cyan(nuxtVersion)}` : ''}:\n`, ) for (const foundModule of results) { - let maxLength = 0 - const entries = Object.entries(foundModule).map(([key, val]) => { + const formattedModule: Record = {} + for (const [key, val] of Object.entries(foundModule)) { const label = upperFirst(kebabCase(key)).replace(/-/g, ' ') - if (label.length > maxLength) { - maxLength = label.length - } - return [label, val || '-'] as const - }) - let infoStr = '' - for (const [label, value] of entries) { - infoStr - += `${bold(label === 'Install' ? '→ ' : '- ') - + green(label.padEnd(maxLength + 2)) - + value - }\n` + formattedModule[label] = val } - logger.info(infoStr) + const title = formattedModule.Name || formattedModule.Package + delete formattedModule.Name + const boxContent = formatInfoBox(formattedModule) + box( + `\n${boxContent}`, + ` ${title} `, + { + contentAlign: 'left', + titleAlign: 'left', + width: 'auto', + titlePadding: 2, + contentPadding: 2, + rounded: true, + }, + ) } } diff --git a/packages/nuxi/src/utils/formatting.ts b/packages/nuxi/src/utils/formatting.ts new file mode 100644 index 000000000..2f538d8a1 --- /dev/null +++ b/packages/nuxi/src/utils/formatting.ts @@ -0,0 +1,96 @@ +import process from 'node:process' +import { stripVTControlCharacters } from 'node:util' +import { colors } from 'consola/utils' + +function getStringWidth(str: string): number { + const stripped = stripVTControlCharacters(str) + let width = 0 + + for (const char of stripped) { + const code = char.codePointAt(0) + if (!code) { + continue + } + + // Variation selectors don't add width + if (code >= 0xFE00 && code <= 0xFE0F) { + continue + } + + // Emoji and wide characters (simplified heuristic) + // Most emojis are in these ranges + if ( + (code >= 0x1F300 && code <= 0x1F9FF) // Emoticons, symbols, pictographs + || (code >= 0x1F600 && code <= 0x1F64F) // Emoticons + || (code >= 0x1F680 && code <= 0x1F6FF) // Transport and map symbols + || (code >= 0x2600 && code <= 0x26FF) // Miscellaneous symbols (includes ❤) + || (code >= 0x2700 && code <= 0x27BF) // Dingbats + || (code >= 0x1F900 && code <= 0x1F9FF) // Supplemental symbols and pictographs + || (code >= 0x1FA70 && code <= 0x1FAFF) // Symbols and Pictographs Extended-A + ) { + width += 2 + } + else { + width += 1 + } + } + + return width +} + +export function formatInfoBox(infoObj: Record): string { + let firstColumnLength = 0 + let ansiFirstColumnLength = 0 + const entries = Object.entries(infoObj).map(([label, val]) => { + if (label.length > firstColumnLength) { + ansiFirstColumnLength = colors.bold(colors.whiteBright(label)).length + 6 + firstColumnLength = label.length + 6 + } + return [label, val || '-'] as const + }) + + // get maximum width of terminal + const terminalWidth = Math.max(process.stdout.columns || 80, firstColumnLength) - 8 /* box padding + extra margin */ + + let boxStr = '' + for (const [label, value] of entries) { + const formattedValue = value + .replace(/\b@([^, ]+)/g, (_, r) => colors.gray(` ${r}`)) + .replace(/`([^`]*)`/g, (_, r) => r) + + boxStr += (`${colors.bold(colors.whiteBright(label))}`).padEnd(ansiFirstColumnLength) + + let boxRowLength = firstColumnLength + + // Split by spaces and wrap as needed + const words = formattedValue.split(' ') + let currentLine = '' + + for (const word of words) { + const wordLength = getStringWidth(word) + const spaceLength = currentLine ? 1 : 0 + + if (boxRowLength + wordLength + spaceLength > terminalWidth) { + // Wrap to next line + if (currentLine) { + boxStr += colors.cyan(currentLine) + } + boxStr += `\n${' '.repeat(firstColumnLength)}` + currentLine = word + boxRowLength = firstColumnLength + wordLength + } + else { + currentLine += (currentLine ? ' ' : '') + word + boxRowLength += wordLength + spaceLength + } + } + + if (currentLine) { + boxStr += colors.cyan(currentLine) + } + + boxStr += '\n' + } + + return boxStr +}