diff --git a/packages/nuxt-cli/src/commands/build.ts b/packages/nuxt-cli/src/commands/build.ts index 8fab2d26f..55b95a2fc 100644 --- a/packages/nuxt-cli/src/commands/build.ts +++ b/packages/nuxt-cli/src/commands/build.ts @@ -122,7 +122,7 @@ export default defineCommand({ const serverBuild = resolveServerBuild(kit, nuxt) const target = serverBuild.target if (target) { - logger.info(`${serverBuild.label} ${serverBuild.targetLabel}: ${styleText('cyan', target)}`) + logger.info(`Deploy target: ${styleText('cyan', target)}`) } const outputDir = serverBuild.dir diff --git a/packages/nuxt-cli/src/commands/dev.ts b/packages/nuxt-cli/src/commands/dev.ts index 9d1014c5a..8c1c619c9 100644 --- a/packages/nuxt-cli/src/commands/dev.ts +++ b/packages/nuxt-cli/src/commands/dev.ts @@ -260,7 +260,7 @@ const command = defineCommand({ throw error }) - const { listener, close, reload, onRestart, onReady, onLoading, onEachReady, onLog, onRequests, onSpans, onRoutes, onBuilding, onReport, onReportClear, onFileChange } = started + const { listener, close, reload, onRestart, onReady, onLoading, onEachReady, onLog, onRequests, onSpans, onRoutes, onStack, onBuilding, onReport, onReportClear, onFileChange } = started /** Feed the dev UI from the server running in this process. */ function attachDevUI(devUI: DevUIController): DevUIController { @@ -273,6 +273,7 @@ const command = defineCommand({ onReport(report => devUI.pushReport(report)) onReportClear(id => devUI.clearReport(id)) onRoutes(payload => devUI.setRoutes(payload)) + onStack(payload => devUI.setStack(payload)) return devUI } @@ -387,6 +388,9 @@ const command = defineCommand({ else if (message.type === 'nuxt:internal:dev:routes') { devUI.setRoutes(message.payload) } + else if (message.type === 'nuxt:internal:dev:stack') { + devUI.setStack(message.payload) + } else if (message.type === 'nuxt:internal:dev:building') { devUI.setStatus(message.building ? 'building' : 'ready') } diff --git a/packages/nuxt-cli/src/commands/info.ts b/packages/nuxt-cli/src/commands/info.ts index 640af78a3..646c037b6 100644 --- a/packages/nuxt-cli/src/commands/info.ts +++ b/packages/nuxt-cli/src/commands/info.ts @@ -1,4 +1,4 @@ -import type { NuxtModule } from '@nuxt/schema' +import type { Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema' import type { PackageJson } from 'pkg-types' import os from 'node:os' @@ -23,10 +23,17 @@ import { readDependencyPackageJson } from '../utils/package-json' import { detectPackageManager, getPackageManagerVersion } from '../utils/package-managers' import { resolveRootDir } from '../utils/paths' import { tryResolveNuxt } from '../utils/resolve-nuxt' +import { getServerBuilderName } from '../utils/server-build' import { rootDirArgs } from './_shared' const LEADING_SLASH_RE = /^\// +/** Server builders other than Nitro released with Nuxt, by each `server.builder` value naming them. */ +const NUXT_SERVER_BUILDERS: Record = { + 'vite': 'vite', + '@nuxt/vite-server': 'vite', +} + /** JSON key for each display label, so `--json` output stays stable if a label is reworded. */ const JSON_KEYS: Record = { 'Operating system': 'operatingSystem', @@ -37,7 +44,7 @@ const JSON_KEYS: Record = { 'nuxt/cli version': 'cliVersion', 'Package manager': 'packageManager', 'Nuxt version': 'nuxtVersion', - 'Nitro version': 'nitroVersion', + 'Server builder': 'serverBuilder', 'Builder': 'builder', 'Config': 'config', 'Modules': 'modules', @@ -94,6 +101,11 @@ export default defineCommand({ const configKeys = Object.keys(nuxtConfig).sort() const moduleNames = modules.filter(module => module !== null) const builder = nuxtConfig.builder || 'vite' + // Read from the config, without loading Nuxt, the same way as for a loaded app. + const usesNitro = getServerBuilderName({ options: nuxtConfig } as unknown as Nuxt) === 'Nitro' + const serverBuilder = usesNitro + ? nitroVersion ? `nitro@${nitroVersion}` : 'nitro' + : await describeServerBuilder(nuxtConfig, getDepVersion) const osType = os.type() const cpus = os.cpus() const builderInfo = typeof builder === 'string' && ['vite', '@nuxt/vite-builder', 'webpack', '@nuxt/webpack-builder', 'rspack', '@nuxt/rspack-builder'].includes(builder) @@ -113,7 +125,7 @@ export default defineCommand({ 'nuxt/cli version': nuxiVersion, 'Package manager': packageManager, 'Nuxt version': nuxtVersion, - 'Nitro version': nitroVersion, + 'Server builder': serverBuilder, 'Builder': builderInfo.name === 'custom' ? 'custom' : `${builderInfo.name.toLowerCase()}@${builderInfo.version}`, 'Config': configKeys.map(key => `\`${key}\``).join(', '), 'Modules': moduleNames.map(name => `\`${name}\``).join(', '), @@ -129,6 +141,7 @@ export default defineCommand({ const key = JSON_KEYS[label]! return [key, lists[key] ?? (value?.replaceAll('`', '') || null)] })), + nitroVersion: (usesNitro && nitroVersion) || null, }) return } @@ -253,3 +266,25 @@ export function normalizeConfigModule( } return null } + +/** + * A server builder other than Nitro: its name when it is released with Nuxt, + * otherwise the package or path `server.builder` names (with the package's + * version), or `custom` for one defined inline. + */ +async function describeServerBuilder( + nuxtConfig: NuxtConfig, + getDepVersion: (name: string) => Promise, +): Promise { + const configured = (nuxtConfig as { server?: { builder?: unknown } }).server?.builder + if (typeof configured !== 'string') { + return 'custom' + } + const nuxtBuilder = NUXT_SERVER_BUILDERS[configured] + if (nuxtBuilder) { + return nuxtBuilder + } + const packageName = getPackageName(configured) + const version = packageName && await getDepVersion(packageName) + return version ? `${configured}@${version}` : configured +} diff --git a/packages/nuxt-cli/src/commands/preview.ts b/packages/nuxt-cli/src/commands/preview.ts index d1b3a0214..151d18919 100644 --- a/packages/nuxt-cli/src/commands/preview.ts +++ b/packages/nuxt-cli/src/commands/preview.ts @@ -73,7 +73,6 @@ const command = defineCommand({ let declared: { label: string target: string | undefined - targetLabel: string dir: string previewCommand: string | undefined staticDir: string | undefined @@ -106,7 +105,6 @@ const command = defineCommand({ declared = { label: build.label, target: build.target, - targetLabel: build.targetLabel, dir: build.dir, previewCommand: build.previewCommand, staticDir: build.previewStaticDir, @@ -162,7 +160,7 @@ const command = defineCommand({ previewCommand = declared.previewCommand outputPath = declared.dir if (declared.target) { - target = [`${declared.label} ${declared.targetLabel}:`, declared.target] + target = ['Deploy target:', declared.target] } } if (declared.staticDir) { @@ -184,7 +182,7 @@ const command = defineCommand({ previewCommand = nitroJSON.commands?.preview outputPath = dirname(nitroJSONPath) if (nitroJSON.preset) { - target = ['Nitro preset:', nitroJSON.preset] + target = ['Deploy target:', nitroJSON.preset] } } } diff --git a/packages/nuxt-cli/src/dev/index.ts b/packages/nuxt-cli/src/dev/index.ts index 677a2558e..4533da6b6 100644 --- a/packages/nuxt-cli/src/dev/index.ts +++ b/packages/nuxt-cli/src/dev/index.ts @@ -6,7 +6,7 @@ import type { DevRestartReason } from './reason' import type { DevReportSummary } from './error-channel' import type { ServerLogEvent } from './log-channel' import type { DevRequestSpan } from './span-channel' -import type { DevRequestEvent, DevRoutes, NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './utils' +import type { DevRequestEvent, DevRoutes, DevStack, NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './utils' import process from 'node:process' @@ -242,6 +242,8 @@ interface InitializeReturn { onBuilding: (callback: (building: boolean) => void) => void /** Called whenever the app's routes are (re)discovered. */ onRoutes: (callback: (routes: DevRoutes) => void) => void + /** Called with the builders the app uses, each time Nuxt loads. */ + onStack: (callback: (stack: DevStack) => void) => void /** Called the first time a watched file changes, before Nuxt reloads. */ onFileChange: (callback: () => void) => void onRestart: (callback: (reason?: DevRestartReason) => void) => void @@ -318,6 +320,7 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti const requests = createFeed(PENDING_REQUEST_BATCHES) const spans = createFeed(PENDING_REQUEST_BATCHES) const routes = createFeed(1) + const stack = createFeed(1) const building = createFeed(0) const reports = createFeed(PENDING_REPORTS) const reportsCleared = createFeed(0) @@ -388,6 +391,14 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti building.emit(value) }) + devServer.on('stack', (payload) => { + if (ipc.enabled) { + ipc.send({ type: 'nuxt:internal:dev:stack', payload }) + return + } + stack.emit(payload) + }) + devServer.on('routes', (payload) => { if (ipc.enabled) { ipc.send({ type: 'nuxt:internal:dev:routes', payload }) @@ -549,6 +560,7 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti onReport: reports.subscribe, onReportClear: reportsCleared.subscribe, onRoutes: routes.subscribe, + onStack: stack.subscribe, onFileChange: (callback: () => void) => { devServer.once('change', callback) }, diff --git a/packages/nuxt-cli/src/dev/tui/controller.ts b/packages/nuxt-cli/src/dev/tui/controller.ts index 4239789f2..41a35d329 100644 --- a/packages/nuxt-cli/src/dev/tui/controller.ts +++ b/packages/nuxt-cli/src/dev/tui/controller.ts @@ -3,7 +3,7 @@ import type { DevReportSummary } from '../error-channel' import type { ServerLogEvent } from '../log-channel' import type { ShortcutContext } from '../shortcuts' import type { DevRequestSpan } from '../span-channel' -import type { DevRequestEvent, DevRoutes } from '../utils' +import type { DevRequestEvent, DevRoutes, DevStack } from '../utils' import type { DevUIOptions } from './index' import type { DevStatus } from './panel' import type { DevUISession } from './session' @@ -33,6 +33,8 @@ export interface DevUIController { clearReport: (id?: string) => void /** Replace the routes shown in the route view. */ setRoutes: (routes: DevRoutes) => void + /** Name the builders the loaded app uses, for the versions in the info view. */ + setStack: (stack: DevStack) => void /** * Report the render the server is busy with, or that it is busy with none. * `awaiting` says no page has been rendered yet, which is what makes it a @@ -52,6 +54,7 @@ export const NOOP_CONTROLLER: DevUIController = { pushReport: () => {}, clearReport: () => {}, setRoutes: () => {}, + setStack: () => {}, setRendering: () => {}, } diff --git a/packages/nuxt-cli/src/dev/tui/index.ts b/packages/nuxt-cli/src/dev/tui/index.ts index 5d6a6111d..2f999c38b 100644 --- a/packages/nuxt-cli/src/dev/tui/index.ts +++ b/packages/nuxt-cli/src/dev/tui/index.ts @@ -1,5 +1,7 @@ +import type { NuxtOptions } from '@nuxt/schema' import type { TerminalNotification } from '../../utils/terminal-host' import type { ShortcutContext } from '../shortcuts' +import type { DevStack } from '../utils' import type { DevUIController } from './controller' import type { PanelStart } from './first-frame' import type { InfoSection } from './info-overlay' @@ -53,6 +55,12 @@ const NOTICE_MS = 4000 const LOADING_STATUSES = new Set(['starting', 'building', 'restarting']) +/** Display names for the server builders released with Nuxt. */ +const SERVER_BUILDER_NAMES: Record = { + nitro: 'Nitro', + vite: 'Vite', +} + interface UIShortcut { keys: string[] /** Short label for the hint line. Omitted shortcuts live only in the help view. */ @@ -122,12 +130,13 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) events, }) let shortcuts: UIShortcut[] = [] + let stack: DevStack | undefined let qrCode: string | undefined let armedOpen = false let ready = false const helpOverlay = new HelpOverlay(() => shortcuts.filter(shortcut => shortcut.isAvailable?.() !== false), write, release) const infoOverlay = new InfoOverlay( - () => describeSession(context, cwd, requests, sessionStart, state.update, state.updateLink), + () => describeSession(context, cwd, requests, sessionStart, stack, state.update, state.updateLink), write, release, () => qrCode, @@ -647,6 +656,9 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) update({ note: undefined }) }, setRoutes: payload => routeOverlay.setRoutes(payload), + setStack: (payload) => { + stack = payload + }, setRendering: (pending, awaiting) => { update({ rendering: pending && { label: pending.label, startedAt: pending.startedAt }, @@ -716,11 +728,17 @@ function describeSession( cwd: string, requests: RequestLog, sessionStart: number, + stack?: DevStack, update?: string, updateLink?: string, ): InfoSection[] { - const versions = resolveStackVersions(cwd) + // Until Nuxt has loaded, the installed packages are all there is to go on. + const versions = resolveStackVersions(cwd, { builder: stack?.builder as NuxtOptions['builder'] | undefined }) const { listener } = context + const server = stack?.server ?? 'nitro' + const serverName = SERVER_BUILDER_NAMES[server] ?? server + // The Vite server builder shares the app builder's version, listed below. + const serverVersion = server === 'nitro' ? versions.nitro : undefined return [ { @@ -729,7 +747,7 @@ function describeSession( ['Nuxt', update ? `${linkVersion(versions.nuxt)} ${paint('warning', updateLink ?? `\u2192 ${update} available`)}` : linkVersion(versions.nuxt)], - ['Nitro', versions.nitro], + ['Server builder', serverVersion ? `${serverName} ${serverVersion}` : serverName], [versions.builder.name, versions.builder.version], [versions.builder.provider?.name ?? 'via', versions.builder.provider?.version], ['Vue', versions.vue ?? undefined], diff --git a/packages/nuxt-cli/src/dev/utils.ts b/packages/nuxt-cli/src/dev/utils.ts index f864c3523..3c1c1c68d 100644 --- a/packages/nuxt-cli/src/dev/utils.ts +++ b/packages/nuxt-cli/src/dev/utils.ts @@ -36,7 +36,7 @@ import { loadKit } from '../utils/kit' import { acquireLock, formatLockError, getTakeoverPid, updateLock } from '../utils/lockfile' import { debug, logger, writeNotice } from '../utils/logger' import { loadNuxtManifest, resolveNuxtManifest, writeNuxtManifest } from '../utils/nuxt' -import { resolveServerBuild } from '../utils/server-build' +import { getServerBuilderId, resolveServerBuild } from '../utils/server-build' import { createConfigWatcher, FileChangeTracker, getLocalLayerDirs } from './config-watcher' import { createCliReport, DEFAULT_ERROR_CHANNEL, ERROR_CHANNEL_ENV, handleErrorChannelRequest, isErrorChannelRequest, isLocalPeer, isThreadRunner, openErrorBridge, publishCliProgress, renderErrorPage, resolveChannelPath, summariseReport, useErrorChannel, withErrorChannel } from './error-channel' import { sendErrorResponse } from './error-response' @@ -68,6 +68,7 @@ export type NuxtDevIPCMessage | { type: 'nuxt:internal:dev:requests', requests: DevRequestEvent[] } | { type: 'nuxt:internal:dev:spans', spans: DevRequestSpan[] } | { type: 'nuxt:internal:dev:routes', payload: DevRoutes } + | { type: 'nuxt:internal:dev:stack', payload: DevStack } | { type: 'nuxt:internal:dev:building', building: boolean } | { type: 'nuxt:internal:dev:rendering', pending?: PendingRender, awaiting?: boolean } | { type: 'nuxt:internal:dev:report', report: DevReportSummary } @@ -181,6 +182,14 @@ export interface DevRoutes { errorComponent?: string } +/** What the loaded app is built with, for the versions the dev UI lists. */ +export interface DevStack { + /** The configured app builder, e.g. `vite` or `@nuxt/webpack-builder`. */ + builder?: string + /** The server builder's short name, e.g. `nitro` or `vite`, when Nuxt has one. */ + server?: string +} + /** A page or server route the app defines, as listed in the dev UI. */ export interface DevRoute { kind: 'page' | 'server' @@ -216,6 +225,7 @@ interface DevServerEventMap { /** Time spent serving a request that the app did not publish itself. */ 'span': [span: DevRequestSpan] 'routes': [payload: DevRoutes] + 'stack': [payload: DevStack] 'building': [building: boolean] /** A report the app forwarded, rendered for a terminal. */ 'report': [report: DevReportSummary] @@ -858,6 +868,7 @@ export class NuxtDevServer extends EventEmitter { this.#currentNuxt = await kit.loadNuxt(loadOptions) if (this.options.captureUIEvents) { + this.#emitStack(this.#currentNuxt) this.#collectRoutes(this.#currentNuxt) this.#watchServerBuilds(this.#currentNuxt) } @@ -885,6 +896,15 @@ export class NuxtDevServer extends EventEmitter { }) } + /** Report which builders the app uses, for the versions the dev UI lists. */ + #emitStack(nuxt: Nuxt): void { + const builder = nuxt.options.builder + this.emit('stack', { + builder: typeof builder === 'string' ? builder : undefined, + server: getServerBuilderId(nuxt), + }) + } + /** * Gather the app's routes. * @@ -1217,6 +1237,11 @@ export class NuxtDevServer extends EventEmitter { await Promise.all([typesPromise, kit.buildNuxt(this.#currentNuxt)]) + // A server builder defined inline describes itself only once it has built. + if (this.options.captureUIEvents) { + this.#emitStack(this.#currentNuxt) + } + if (!this.#currentNuxt.server) { throw new ActionableError(noDevServerMessage(serverBuild.label, true)) } diff --git a/packages/nuxt-cli/src/utils/banner.ts b/packages/nuxt-cli/src/utils/banner.ts index 59d17c21e..8613dfc40 100644 --- a/packages/nuxt-cli/src/utils/banner.ts +++ b/packages/nuxt-cli/src/utils/banner.ts @@ -5,6 +5,7 @@ import { styleText } from 'node:util' import { logger } from './logger' import { getNitroVersion } from './nitro' import { getNuxtPkgVersion, getPkgJSON, getPkgVersion } from './pkg' +import { getServerBuilderName } from './server-build' export function getBuilder(cwd: string, builder: Exclude): { name: string, version: string, provider?: { name: string, version: string } } { switch (builder) { @@ -30,7 +31,12 @@ export function getBuilder(cwd: string, builder: Exclude string | undefined - targetLabel?: string output: { dir: () => string, publicDir: () => string } capabilities: { server: boolean, dev: boolean } preview?: { @@ -97,10 +91,7 @@ export function tryUseNitro(kit: MaybeModernKit): NitroLike | undefined { * reads as `vite`. */ export function getServerBuilderName(nuxt: Nuxt, hasServer?: boolean): string { - const declared = (nuxt as MaybeModernNuxt).serverBuild - // A descriptor Nuxt seeded but no builder replaced carries the raw specifier - // as its name, so it is read the same way as the `server.builder` option. - const name = declared ? declared.label || normalizeBuilderName(declared.name) : inferBuilderName(nuxt) + const name = (nuxt as MaybeModernNuxt).serverBuild?.label || getServerBuilderId(nuxt) if (name === 'nitro') { return 'Nitro' } @@ -111,6 +102,17 @@ export function getServerBuilderName(nuxt: Nuxt, hasServer?: boolean): string { return hasServer === false ? 'unknown' : 'Nitro' } +/** + * The configured server builder's short name, such as `nitro`, `vite` or + * `custom`, or `undefined` for Nuxt versions without `server.builder`. + */ +export function getServerBuilderId(nuxt: Nuxt): string | undefined { + const declared = (nuxt as MaybeModernNuxt).serverBuild + // A descriptor Nuxt seeded but no builder replaced carries the raw specifier + // as its name, so it is read the same way as the `server.builder` option. + return declared ? normalizeBuilderName(declared.name) : inferBuilderName(nuxt) +} + /** * The server builder named by the `server.builder` option, for Nuxt versions * that do not describe the build themselves. @@ -142,7 +144,6 @@ export function resolveServerBuild(kit: MaybeModernKit, nuxt: Nuxt): ServerBuild const getNitro = () => declared?.capabilities.server === false ? undefined : tryUseNitro(kit) return { - targetLabel: declared?.targetLabel ?? DEFAULT_TARGET_LABEL, declared: !!declared, hasDevServer: declared?.capabilities.dev ?? true, get label() { diff --git a/packages/nuxt-cli/test/unit/commands/build.spec.ts b/packages/nuxt-cli/test/unit/commands/build.spec.ts index d39d27c77..0b5bf8ef0 100644 --- a/packages/nuxt-cli/test/unit/commands/build.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/build.spec.ts @@ -157,7 +157,7 @@ describe('build', () => { })) }) - it('names the deploy target as the builder does', async () => { + it('names the deploy target the server builder declares', async () => { mocks.buildNuxt.mockResolvedValue(undefined) mocks.loadNuxt.mockResolvedValue({ hook: vi.fn(), @@ -175,8 +175,7 @@ describe('build', () => { await run() - expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Nitro preset:')) - expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('vercel')) + expect(logger.info).toHaveBeenCalledWith(expect.stringMatching(/^Deploy target: .*vercel/)) }) it('builds without a server, locking the default output directory', async () => { diff --git a/packages/nuxt-cli/test/unit/commands/dev-run.spec.ts b/packages/nuxt-cli/test/unit/commands/dev-run.spec.ts index 133704176..b0d646a55 100644 --- a/packages/nuxt-cli/test/unit/commands/dev-run.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/dev-run.spec.ts @@ -109,6 +109,7 @@ beforeEach(() => { onRequests: vi.fn(), onSpans: vi.fn(), onRoutes: vi.fn(), + onStack: vi.fn(), onBuilding: vi.fn(), onReport: vi.fn(), onReportClear: vi.fn(), diff --git a/packages/nuxt-cli/test/unit/commands/info-run.spec.ts b/packages/nuxt-cli/test/unit/commands/info-run.spec.ts index d2ef904b8..f3bb13df8 100644 --- a/packages/nuxt-cli/test/unit/commands/info-run.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/info-run.spec.ts @@ -96,6 +96,41 @@ describe('info command', () => { } }) + // Versions are declared so the fixtures resolve one even where nothing is installed. + it('should name Nitro as the default server builder', async () => { + await writeFile(join(cwd, 'package.json'), JSON.stringify({ name: 'app', private: true, dependencies: { nitropack: '2.13.6' } })) + + const payload = await runInfoJSON() + + expect(payload.serverBuilder).toMatch(/^nitro@\d/) + expect(payload.nitroVersion).toMatch(/^\d/) + }) + + it.each(['@nuxt/vite-server', 'vite'])('should name the Vite server builder from `%s` without a version', async (builder) => { + await writeFile(join(cwd, 'package.json'), JSON.stringify({ name: 'app', private: true, dependencies: { '@nuxt/vite-server': '4.6.4' } })) + await writeFile(join(cwd, 'nuxt.config.mjs'), `export default { server: { builder: '${builder}' } }`) + + const payload = await runInfoJSON() + + expect(payload.serverBuilder).toBe('vite') + expect(payload.nitroVersion).toBeNull() + }) + + it('should name a third-party server builder with its version', async () => { + await writeFile(join(cwd, 'package.json'), JSON.stringify({ name: 'app', private: true, dependencies: { 'my-server-builder': '1.2.3' } })) + await writeFile(join(cwd, 'nuxt.config.mjs'), `export default { server: { builder: 'my-server-builder' } }`) + + expect((await runInfoJSON()).serverBuilder).toBe('my-server-builder@1.2.3') + }) + + it('should name a local server builder by its path alone', async () => { + await writeFile(join(cwd, 'package.json'), JSON.stringify({ name: 'app', version: '9.9.9', private: true })) + await writeFile(join(cwd, 'server-builder.mjs'), 'export async function bundle() {}') + await writeFile(join(cwd, 'nuxt.config.mjs'), `export default { server: { builder: './server-builder.mjs' } }`) + + expect((await runInfoJSON()).serverBuilder).toBe('./server-builder.mjs') + }) + it('should still report on a project with no config', async () => { await writeFile(join(cwd, 'package.json'), JSON.stringify({ name: 'app', private: true })) diff --git a/packages/nuxt-cli/test/unit/commands/preview.spec.ts b/packages/nuxt-cli/test/unit/commands/preview.spec.ts index ff8bd0168..d990f2dc9 100644 --- a/packages/nuxt-cli/test/unit/commands/preview.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/preview.spec.ts @@ -11,6 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import preview from '../../../src/commands/preview' import { previewLockDir, readLock } from '../../../src/utils/lockfile' import { logger } from '../../../src/utils/logger' +import { render, screen } from '../../utils/terminal' const { checkPort, getPort, loadKit, loadNuxt, x } = vi.hoisted(() => ({ checkPort: vi.fn<(port: number, host?: string) => Promise>(), @@ -129,6 +130,36 @@ describe('preview', () => { }) }) + it('names the deploy target a Nitro build recorded', async () => { + await writeNitroJSON(join(cwd, '.output'), { preset: 'cloudflare-module' }) + + const terminal = await render(() => runCommand(preview, { rawArgs: [cwd] })) + + expect(screen(terminal)).toMatch(/Deploy target:\s+cloudflare-module/) + }) + + it('names the deploy target the server builder declares', async () => { + const outputDir = join(cwd, '.output') + await mkdir(outputDir, { recursive: true }) + loadNuxt.mockResolvedValue({ + options: { srcDir: cwd }, + hook: vi.fn(), + close: vi.fn(), + serverBuild: { + name: 'vite', + label: 'Vite server', + target: () => 'cloudflare', + capabilities: { server: true, dev: true }, + output: { dir: () => outputDir, publicDir: () => join(outputDir, 'public') }, + preview: { command: () => 'npx wrangler dev' }, + }, + }) + + const terminal = await render(() => runCommand(preview, { rawArgs: [cwd] })) + + expect(screen(terminal)).toMatch(/Deploy target:\s+cloudflare/) + }) + it('uses the configured output directory', async () => { const outputDir = join(cwd, 'dist', 'server-output') await writeNitroJSON(outputDir) diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index ce26260f2..e28d89ca1 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -36,6 +36,9 @@ vi.mock('../../src/dev/listen', async importOriginal => ({ openBrowser: (url: string) => void opened.push(url), })) +// The info view resolves versions from what is installed; pinned so it lists Nitro. +vi.mock('../../src/utils/nitro', () => ({ getNitroVersion: () => '2.13.5' })) + const copied: string[] = [] vi.mock('tinyclip', () => ({ writeText: (text: string) => { @@ -3013,6 +3016,22 @@ async function withPanel(run: (ui: ReturnType, settle: () => } } +describe('panel info view', () => { + it('lists Nitro until the app names another server builder', async () => { + await withPanel(async (_ui, settle) => { + process.stdin.emit('keypress', 'i', { name: 'i', sequence: 'i' }) + expect(await settle()).toMatch(/Server builder\s+Nitro 2\.13\.5/) + }) + await withPanel(async (ui, settle) => { + ui.setStack({ builder: 'vite', server: 'vite' }) + process.stdin.emit('keypress', 'i', { name: 'i', sequence: 'i' }) + const frame = await settle() + expect(frame).toMatch(/Server builder\s+Vite\b/) + expect(frame).not.toContain('Nitro') + }) + }) +}) + describe('panel shortcut routing', () => { it.each(['ready', 'starting', 'building', 'restarting', 'warming', 'error'] as const)('quits without confirmation while %s', async (status) => { const interrupt = vi.fn() diff --git a/packages/nuxt-cli/test/unit/dev/initialize.spec.ts b/packages/nuxt-cli/test/unit/dev/initialize.spec.ts index 780722d55..77550a640 100644 --- a/packages/nuxt-cli/test/unit/dev/initialize.spec.ts +++ b/packages/nuxt-cli/test/unit/dev/initialize.spec.ts @@ -152,4 +152,19 @@ describe('initialize', () => { expect(changed).toHaveBeenCalledTimes(1) expect(restarted).toHaveBeenCalledWith({ type: 'hook' }) }) + + it('should replay the latest stack and forward later ones', async () => { + const { onStack } = await initialize(context(), { captureUIEvents: true }) + devServers[0].emit('stack', { builder: 'vite', server: 'nitro' }) + devServers[0].emit('stack', { builder: 'vite', server: 'vite' }) + + const seen: unknown[] = [] + onStack(stack => seen.push(stack)) + devServers[0].emit('stack', { builder: 'webpack', server: 'nitro' }) + + expect(seen).toEqual([ + { builder: 'vite', server: 'vite' }, + { builder: 'webpack', server: 'nitro' }, + ]) + }) }) diff --git a/packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts b/packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts index 1b2aeeb3d..5e658c34a 100644 --- a/packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts +++ b/packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts @@ -276,6 +276,26 @@ describe('dev server request feed', () => { }) }) +describe('dev server stack', () => { + it('should report the server builder on load and again once it has built', async () => { + const nuxt = createNuxt({ builder: 'vite', server: { builder: { bundle: () => {} } } }) as FakeNuxt & { serverBuild?: unknown } + loadNuxt.mockImplementation(() => Promise.resolve(nuxt)) + buildNuxt.mockImplementation(async () => { + nuxt.serverBuild = { name: 'nitro', label: 'Nitro', capabilities: { server: true, dev: true } } + }) + const server = createServer({ captureUIEvents: true }) + const seen: unknown[] = [] + server.on('stack', stack => seen.push(stack)) + + await server.init() + + expect(seen).toEqual([ + { builder: 'vite', server: 'custom' }, + { builder: 'vite', server: 'nitro' }, + ]) + }) +}) + describe('dev server failures', () => { it('should surface a first-run failure to the caller', async () => { loadNuxt.mockImplementation(() => Promise.reject(new Error('config exploded'))) diff --git a/packages/nuxt-cli/test/unit/utils/banner.spec.ts b/packages/nuxt-cli/test/unit/utils/banner.spec.ts index 9e6a8e84a..08d70b03a 100644 --- a/packages/nuxt-cli/test/unit/utils/banner.spec.ts +++ b/packages/nuxt-cli/test/unit/utils/banner.spec.ts @@ -142,6 +142,16 @@ describe('showBanner', () => { `) }) + it('should omit nitro when another server builder is configured', async () => { + const renderer = await render(() => + showBanner({ _version: '4.4.6', options: { rootDir: '/any', builder: 'vite', server: { builder: '@nuxt/vite-server' } } } as unknown as Nuxt)) + + expect(screen(renderer)).toMatchInlineSnapshot(` + "│ + ● Nuxt 4.4.6 (with Vite 7.3.1 and Vue 3.5.39)" + `) + }) + it('should identify Vite+ as the Vite provider', async () => { const renderer = await render(() => showBanner({ _version: '4.4.6', options: { rootDir: '/vite-plus', builder: 'vite' } } as unknown as Nuxt)) diff --git a/packages/nuxt-cli/test/unit/utils/server-build.spec.ts b/packages/nuxt-cli/test/unit/utils/server-build.spec.ts index befd0fedd..5639229f9 100644 --- a/packages/nuxt-cli/test/unit/utils/server-build.spec.ts +++ b/packages/nuxt-cli/test/unit/utils/server-build.spec.ts @@ -91,7 +91,7 @@ describe('resolveServerBuild', () => { }) const build = resolveServerBuild(nitroKit, nuxt) - expect(build).toMatchObject({ label: 'Nitro', targetLabel: 'preset', declared: true, hasDevServer: true }) + expect(build).toMatchObject({ label: 'Nitro', declared: true, hasDevServer: true }) expect(build.target).toBe('vercel') expect(build.previewCommand).toBe('node ./server/index.mjs') expect(build.dir).toBe('/project/.vercel/output')