Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions packages/create-nuxt/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import type { DownloadTemplateResult } from 'giget'
import type { PackageManagerName } from 'nypm'
import type { InstallResult } from '../../nuxt-cli/src/utils/install'
import type { TemplateData } from '../../nuxt-cli/src/utils/starter-templates'

import { existsSync } from 'node:fs'

import { writeFile } from 'node:fs/promises'
import process from 'node:process'

import { styleText } from 'node:util'
import { cancel, confirm, intro, isCancel, outro, S_BAR, select, spinner, text } from '@clack/prompts'

import { cancel, confirm, intro, isCancel, outro, S_BAR, select, text } from '@clack/prompts'
import { defineCommand, showUsage } from 'citty'
import { downloadTemplate, startShell } from 'giget'
import { detectPackageManager } from 'nypm'
import { basename, join, relative, resolve } from 'pathe'
import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types'
import { hasTTY } from 'std-env'
import { x } from 'tinyexec'

import { cwdArgs, logLevelArgs } from '../../nuxt-cli/src/commands/_shared'

import { selectModulesAutocomplete } from '../../nuxt-cli/src/commands/module/_autocomplete'
import { checkNuxtCompatibility, fetchModules, MODULES_API_URL } from '../../nuxt-cli/src/commands/module/_utils'
import addModuleCommand from '../../nuxt-cli/src/commands/module/add'
Expand All @@ -30,6 +30,7 @@ import { createInstallLog, resolvePackageManagerDescriptor, runInstall, takeUnre
import { debug, logger } from '../../nuxt-cli/src/utils/logger'
import { classifyNetworkError, describeNetworkError, logNetworkError, probeNetworkError } from '../../nuxt-cli/src/utils/network'
import { relativeToProcess } from '../../nuxt-cli/src/utils/paths'
import { createSpinner } from '../../nuxt-cli/src/utils/spinner'
import { getTemplates, TEMPLATES_API_URL } from '../../nuxt-cli/src/utils/starter-templates'
import { paint } from '../../nuxt-cli/src/utils/terminal-theme'
import { getNuxtVersion } from '../../nuxt-cli/src/utils/versions'
Expand Down Expand Up @@ -236,7 +237,7 @@ export default defineCommand({
availableTemplates = defaultTemplates
}
else {
const templatesSpinner = spinner()
const templatesSpinner = createSpinner()
templatesSpinner.start('Loading available templates')

try {
Expand Down Expand Up @@ -384,7 +385,7 @@ export default defineCommand({

const registry = process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY

const downloadSpinner = spinner()
const downloadSpinner = createSpinner()
downloadSpinner.start(`Downloading ${styleText('cyan', templateName)} template`)

try {
Expand Down Expand Up @@ -436,7 +437,7 @@ export default defineCommand({
}

if (ctx.args.nightly !== undefined && !ctx.args.offline && !ctx.args.preferOffline) {
const nightlySpinner = spinner()
const nightlySpinner = createSpinner()
nightlySpinner.start('Fetching nightly version info')

const response = await fetchJson<{ 'dist-tags': Record<string, string> }>(NIGHTLY_DIST_TAGS_URL).catch((err) => {
Expand Down Expand Up @@ -568,7 +569,7 @@ export default defineCommand({
else {
const installController = new AbortController()
const installLog = createInstallLog({ verbose: isVerbose(ctx.args.logLevel) })
const installSpinner = spinner({
const installSpinner = createSpinner({
indicator: 'timer',
onCancel: () => installController.abort(),
})
Expand Down Expand Up @@ -619,7 +620,7 @@ export default defineCommand({
}

if (gitInit) {
const gitSpinner = spinner()
const gitSpinner = createSpinner()
gitSpinner.start('Initializing git repository')

const git = await x('git', ['init'], {
Expand Down Expand Up @@ -674,7 +675,7 @@ export default defineCommand({
prompted = true

if (wantsUserModules) {
const modulesSpinner = spinner()
const modulesSpinner = createSpinner()
modulesSpinner.start('Fetching available modules')

const [response, templateDeps, nuxtVersion] = await Promise.all([
Expand Down
5 changes: 3 additions & 2 deletions packages/nuxt-cli/src/commands/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { writeFile } from 'node:fs/promises'
import process from 'node:process'

import { styleText } from 'node:util'
import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts'
import { cancel, confirm, isCancel, select } from '@clack/prompts'
import { defineCommand } from 'citty'
import { resolveModulePath } from 'exsolve'
import { addDevDependency, detectPackageManager } from 'nypm'
Expand All @@ -18,6 +18,7 @@ import { loadKit } from '../utils/kit'
import { logger } from '../utils/logger'
import { resolveRootDir } from '../utils/paths'
import { withNodePath } from '../utils/resolve-nuxt'
import { createSpinner } from '../utils/spinner'
import { dotEnvArgs, extendsArgs, logLevelArgs, rootDirArgs } from './_shared'

type TypeChecker = 'vue-tsc' | 'golar'
Expand Down Expand Up @@ -355,7 +356,7 @@ async function installMissingPackages(options: {
return false
}

const spin = spinner()
const spin = createSpinner()
spin.start(`Installing ${list} with ${styleText('cyan', pmName)}`)
try {
await addDevDependency(packages, { cwd, packageManager, silent: true })
Expand Down
17 changes: 9 additions & 8 deletions packages/nuxt-cli/src/commands/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import type { PackageJson } from 'pkg-types'

import type { UpdateCatalogEntriesResult } from '../utils/catalog'
import type { InstallResult } from '../utils/install'

import { existsSync } from 'node:fs'
import process from 'node:process'

import process from 'node:process'
import { styleText } from 'node:util'
import { cancel, isCancel, note, select, spinner } from '@clack/prompts'

import { cancel, isCancel, note, select } from '@clack/prompts'
import { defineCommand } from 'citty'
import { detectPackageManager } from 'nypm'
import { dirname, relative, resolve } from 'pathe'
import { findWorkspaceDir, readPackageJSON } from 'pkg-types'

import { resolveCatalogEntry, updateCatalogEntries } from '../utils/catalog'

import { createInstallLog, runDedupe, runInstall, takeUnreportedIgnoredBuilds } from '../utils/install'
import { loadKit } from '../utils/kit'
import { intro, logger, outro } from '../utils/logger'
import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt'
import { getPackageManagerVersion } from '../utils/packageManagers'
import { relativeToProcess, resolveRootDir } from '../utils/paths'
import { createSpinner } from '../utils/spinner'
import { getNuxtVersion, resolveRegistryVersion } from '../utils/versions'
import { logLevelArgs, rootDirArgs } from './_shared'

Expand Down Expand Up @@ -269,7 +270,7 @@ export default defineCommand({
let catalogResult: UpdateCatalogEntriesResult | 'skipped' = 'skipped'

if (catalogUpdates.length > 0) {
const catalogSpinner = spinner()
const catalogSpinner = createSpinner()
catalogSpinner.start('Updating catalog entries')

const resolved: Array<{ catalog: string, pkg: string, specifier: string }> = []
Expand Down Expand Up @@ -352,7 +353,7 @@ export default defineCommand({
}
}

const cleanupSpinner = spinner()
const cleanupSpinner = createSpinner()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cleanupSpinner.start('Cleaning up build directories')
let buildDir: string = '.nuxt'
try {
Expand All @@ -368,7 +369,7 @@ export default defineCommand({
cleanupSpinner.stop('Build directories cleaned')
}
catch (err) {
cleanupSpinner.stop('Could not clean build directories')
cleanupSpinner.error('Could not clean build directories')
logger.warn(`Nuxt was upgraded but build directories could not be removed: ${err instanceof Error ? err.message : err}`)
}

Expand Down Expand Up @@ -423,7 +424,7 @@ async function withInstallSpinner(
): Promise<boolean> {
const controller = new AbortController()
const installLog = createInstallLog({ verbose: options.verbose })
const spin = spinner({
const spin = createSpinner({
indicator: 'timer',
onCancel: () => controller.abort(),
})
Expand Down
5 changes: 3 additions & 2 deletions packages/nuxt-cli/src/dev/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PackageManager } from 'nypm'
import { accessSync, constants, existsSync, readdirSync, readFileSync } from 'node:fs'
import { styleText } from 'node:util'

import { confirm, isCancel, spinner } from '@clack/prompts'
import { confirm, isCancel } from '@clack/prompts'
import { dirname, join } from 'pathe'

import { restoreRawMode, withDirectStdout } from '../utils/console'
Expand All @@ -12,6 +12,7 @@ import { debug, logger } from '../utils/logger'
import { CONFIG_EXTENSIONS } from '../utils/nuxt-config'
import { relativeTo } from '../utils/paths'
import { tryResolveNuxt } from '../utils/resolve-nuxt'
import { createSpinner } from '../utils/spinner'
import { withUserAttention } from '../utils/startup-clock'
import { isInteractive } from '../utils/stdout'

Expand Down Expand Up @@ -253,7 +254,7 @@ async function offerInstall(cwd: string, interactive: boolean): Promise<void> {

const controller = new AbortController()
const installLog = createInstallLog()
const installSpinner = spinner({ indicator: 'timer', onCancel: () => controller.abort() })
const installSpinner = createSpinner({ indicator: 'timer', onCancel: () => controller.abort() })
installSpinner.start(`Installing with ${styleText('cyan', packageManager.name)}`)

const result = await runInstall({
Expand Down
14 changes: 12 additions & 2 deletions packages/nuxt-cli/src/run.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { CommandDef } from 'citty'

import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { stripVTControlCharacters } from 'node:util'

import { runCommand as _runCommand, runMain as _runMain } from 'citty'
import { runCommand as _runCommand, runMain as _runMain, renderUsage } from 'citty'

import { commands } from './commands'
import { globalCwdArgs } from './commands/_shared'
Expand Down Expand Up @@ -34,7 +37,7 @@ export async function runMain(): Promise<void> {
const { initCompletions } = await import('./completions')
await initCompletions(main)
}
await _runMain(main)
await _runMain(main, { showUsage })

if (!currentCommand || !LONG_RUNNING_COMMANDS.has(currentCommand)) {
warnOnHang({ action: currentCommand ? `\`nuxt ${currentCommand}\`` : 'command' })
Expand All @@ -58,3 +61,10 @@ export async function runCommand(
},
})
}

/** Print usage, unstyled when stdout lacks colour support. */
export async function showUsage(cmd: CommandDef<any>, parent?: CommandDef<any>): Promise<void> {
const usage = await renderUsage(cmd, parent)
const plain = process.env.NO_COLOR || !process.stdout.hasColors?.()
process.stdout.write(`${plain ? stripVTControlCharacters(usage) : usage}\n\n`)
}
64 changes: 36 additions & 28 deletions packages/nuxt-cli/src/utils/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,51 +21,32 @@ export interface Spinner {
* says what it is doing. Stopped with `options.done`, or silently, once `fn`
* settles.
*
* Anywhere the frames would be noise rather than animation (CI, an agent, a piped
* log) each message is logged as a plain line instead.
* Without a TTY or in CI, each message is logged once as a plain line.
*/
export async function withSpinner<T>(message: string, fn: (spinner: Spinner) => Promise<T>, options: { done?: string } = {}): Promise<T> {
let done = options.done
const setDone = (text: string) => {
done = text
}

const host = useTerminalHost()
if (host) {
const task = host.startTask(message)
const indicator = createSpinner()
const run = async () => {
indicator.start(message)
try {
const result = await fn({ update: text => task.update(text), done: setDone })
task.stop(done, 'success')
const result = await fn({ update: text => indicator.message(text), done: setDone })
indicator.stop(done)
return result
}
catch (error) {
// The message the work chose describes it succeeding; whoever threw
// reports the failure itself.
task.stop(undefined, 'failure')
indicator.error()
throw error
}
}

if (!process.stdout.isTTY || isCI) {
logger.info(`${message}...`)
const result = await fn({ update: text => logger.info(`${text}...`), done: setDone })
if (done) {
logger.info(done)
}
return result
}

return withDirectStdout(async () => {
const indicator = spinner()
indicator.start(message)
try {
return await fn({ update: text => indicator.message(text), done: setDone })
}
finally {
indicator.stop(done)
restoreRawMode()
}
})
}
return useTerminalHost() ? run() : withDirectStdout(run)
}

export interface CliSpinner {
Expand All @@ -84,12 +65,17 @@ export interface CliSpinner {
* history one frame at a time. With a terminal host published, the work is
* reported as a task on the host's own status line instead.
*
* Without a TTY or in CI, each distinct message is logged once as a plain line.
*
* A host implies an interactive terminal, so cancellation stays with its key
* handling: `onCancel` only fires on the clack path.
*/
export function createSpinner(options: { indicator?: 'dots' | 'timer', onCancel?: () => void } = {}): CliSpinner {
const host = useTerminalHost()
if (!host) {
if (!process.stdout.isTTY || isCI) {
return createPlainSpinner()
}
return spinner(options)
}
let task: TerminalTask | undefined
Expand All @@ -106,3 +92,25 @@ export function createSpinner(options: { indicator?: 'dots' | 'timer', onCancel?
},
}
}

function createPlainSpinner(): CliSpinner {
let last: string | undefined
const log = (text: string) => {
if (text !== last) {
last = text
logger.info(`${text}...`)
}
}
const finish = (report: (message: string) => void) => (message?: string) => {
if (message) {
report(message)
}
last = undefined
}
return {
start: log,
message: log,
stop: finish(message => logger.success(message)),
error: finish(message => logger.error(message)),
}
}
23 changes: 22 additions & 1 deletion packages/nuxt-cli/test/unit/help.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { CommandDef, Resolvable } from 'citty'

import { styleText } from 'node:util'

import { renderUsage } from 'citty'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

import { commands } from '../../src/commands'
import { main } from '../../src/main'
import { showUsage } from '../../src/run'

async function resolve(def: Resolvable<CommandDef>): Promise<CommandDef> {
return typeof def === 'function' ? await def() : def
Expand Down Expand Up @@ -528,3 +531,21 @@ describe('help', () => {
`)
})
})

describe('showUsage', () => {
it('should drop ANSI styling when stdout does not support colour', async () => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
const hasColors = process.stdout.hasColors
process.stdout.hasColors = () => false
try {
await showUsage({ meta: { name: 'nuxt', description: styleText('bold', 'Nuxt CLI', { validateStream: false }) } })
const output = String(write.mock.calls[0]![0])
expect(output).toContain('Nuxt CLI')
expect(output).not.toContain('\u001B[')
}
finally {
write.mockRestore()
process.stdout.hasColors = hasColors
}
})
})
Loading
Loading