diff --git a/knip.json b/knip.json index 7319ee279..3a90819d6 100644 --- a/knip.json +++ b/knip.json @@ -34,6 +34,7 @@ "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 35866319f..478c63c44 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -33,11 +33,12 @@ }, "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", "@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", @@ -45,6 +46,7 @@ "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", diff --git a/packages/nuxi/src/commands/add.ts b/packages/nuxi/src/commands/add.ts index 8bd0d449e..215d70114 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 { colors } from 'consola/utils' import { dirname, extname, resolve } from 'pathe' 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..d76f59cf8 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -3,7 +3,9 @@ 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 { colors } from 'consola/utils' import { defu } from 'defu' import { H3, lazyEventHandler } from 'h3-next' import { join, resolve } from 'pathe' @@ -13,6 +15,7 @@ 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.`, 'Build location') 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 build complete!') + } }, }) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index ec2942294..71ca2b12b 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -2,7 +2,9 @@ import type { Nitro } from 'nitropack' 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 { showVersions } from '../utils/banner' @@ -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..1eedcade4 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -4,6 +4,7 @@ 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 { satisfies } from 'semver' @@ -12,7 +13,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 +107,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 +125,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') { @@ -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..3ca1b5e7d 100644 --- a/packages/nuxi/src/commands/devtools.ts +++ b/packages/nuxi/src/commands/devtools.ts @@ -1,6 +1,7 @@ import process from 'node:process' import { defineCommand } from 'citty' +import { colors } from 'consola/utils' import { resolve } from 'pathe' import { x } from 'tinyexec' @@ -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/info.ts b/packages/nuxi/src/commands/info.ts index 4155e7691..b29b59070 100644 --- a/packages/nuxi/src/commands/info.ts +++ b/packages/nuxi/src/commands/info.ts @@ -4,16 +4,19 @@ import type { PackageJson } from 'pkg-types' import os from 'node:os' import process from 'node:process' +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 { 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 { formatInfoBox } from '../utils/formatting' 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,88 @@ 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.info(`Nuxt root directory: ${colors.cyan(nuxtConfig.rootDir || cwd)}\n`) - logger.log('Working directory:', cwd) + const boxStr = formatInfoBox(infoObj) - 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 secondColumnLength = 0 + const entries = Object.entries(infoObj).map(([label, val]) => { + if (label.length > firstColumnLength) { + firstColumnLength = label.length + 4 + } + if ((val || '').length > secondColumnLength) { + secondColumnLength = (val || '').length + 2 } return [label, val || '-'] as const }) - let infoStr = '' + + // formatted for copy-pasting into an issue + let copyStr = `| ${' '.repeat(firstColumnLength)} | ${' '.repeat(secondColumnLength)} |\n| ${'-'.repeat(firstColumnLength)} | ${'-'.repeat(secondColumnLength)} |\n` for (const [label, value] of entries) { - infoStr - += `- ${ - (`${label}: `).padEnd(maxLength + 2) - }${value.includes('`') ? value : `\`${value}\`` - }\n` + if (!isMinimal) { + copyStr += `| ${`**${label}**`.padEnd(firstColumnLength)} | ${(value.includes('`') ? value : `\`${value}\``).padEnd(secondColumnLength)} |\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..86f280508 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -4,7 +4,7 @@ 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' @@ -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' @@ -42,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 [] } } @@ -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 @@ -203,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' }, @@ -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) { @@ -299,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) } @@ -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) } } @@ -500,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 a42a607a0..34b8df67a 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) @@ -146,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, }) @@ -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 { 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 nuxi 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.log(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/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index 7c9ac9a79..ed1cff5ba 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 { colors } from 'consola/utils' +import { resolve } from 'pathe' 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..1ed966d79 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 { colors } from 'consola/utils' import { resolve } from 'pathe' 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' @@ -109,12 +114,12 @@ const command = defineCommand({ if (envExists) { logger.info( - `Loading \`${envFileName}\`. This will not be loaded when running the server in production.`, + `Loading ${colors.cyan(envFileName)}. This will not be loaded when running the server in production.`, ) await setupDotenv({ cwd, fileName: envFileName }) } else if (ctx.args.dotenv) { - logger.error(`Cannot find \`${envFileName}\`.`) + logger.error(`Cannot find ${colors.cyan(envFileName)}.`) } const port = ctx.args.port @@ -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/test.ts b/packages/nuxi/src/commands/test.ts index 8f109cf07..a755ab691 100644 --- a/packages/nuxi/src/commands/test.ts +++ b/packages/nuxi/src/commands/test.ts @@ -58,6 +58,6 @@ async function importTestUtils(): 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/commands/upgrade.ts b/packages/nuxi/src/commands/upgrade.ts index c070c3e8d..71fee65d7 100644 --- a/packages/nuxi/src/commands/upgrade.ts +++ b/packages/nuxi/src/commands/upgrade.ts @@ -3,7 +3,7 @@ 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' @@ -14,6 +14,7 @@ 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..e6f7fc595 100644 --- a/packages/nuxi/src/main.ts +++ b/packages/nuxi/src/main.ts @@ -13,7 +13,7 @@ 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 +47,8 @@ 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 }) + setupGlobalConsole({ dev: command === '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..980746235 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 { 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/engines.ts b/packages/nuxi/src/utils/engines.ts index 5558efc4e..167c7d82e 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 'consola/utils' 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..7f9e7ea97 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 'consola/utils' 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.`, ) } 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 +} 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 9c3dc2a8f..a0ba8266d 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -34,12 +34,13 @@ }, "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", "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", @@ -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..8bddb2601 100644 --- a/packages/nuxt-cli/src/main.ts +++ b/packages/nuxt-cli/src/main.ts @@ -9,8 +9,8 @@ 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({ @@ -29,8 +29,7 @@ const _main = defineCommand({ subCommands: commands, async setup(ctx) { const command = ctx.args._[0] - const dev = command === 'dev' - setupGlobalConsole({ dev }) + 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 1b7eab0ad..36fbec6ee 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 @@ -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 @@ -155,6 +158,9 @@ importers: 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 @@ -252,7 +258,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 c12: specifier: ^3.3.1 @@ -269,6 +275,9 @@ importers: 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 @@ -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