Skip to content
Closed
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
48 changes: 37 additions & 11 deletions packages/nuxt-cli/src/utils/build-progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const HOOK_PHASES: Record<string, string> = {
'builder:generateApp': 'app',
'prepare:types': 'types',
'build:before': 'client',
'nitro:build:before': 'nitro',
}

/** Nitro builds on its own hooks, which Nuxt's never see. */
Expand All @@ -45,10 +44,14 @@ interface HookableLike {
}

interface NuxtLike {
options?: { experimental?: { viteEnvironmentApi?: boolean } }
hooks?: HookableLike
}

interface VitePluginContext {
/** Missing before Vite 6. */
environment?: { name: string }
}

/**
* Tracks how far a production build has got, so the same transient phase line
* `nuxt dev` shows can report `nuxt build`, and the completion summary can list
Expand All @@ -58,11 +61,10 @@ export class BuildProgress extends PhaseTimeline {
#message = BUILD_PHASES[0]!.message
#finished = false
/**
* With the Vite environment API a single build covers both environments, so
* the client and server configs resolve up front and cannot mark the boundary
* between them.
* Vite can resolve every config, and start Nitro's build, before it bundles
* anything, so with Vite only its bundle hooks mark the phases.
*/
#splitBundles = true
#bundledByVite = false

constructor() {
super(BUILD_PHASES)
Expand Down Expand Up @@ -110,16 +112,19 @@ export class BuildProgress extends PhaseTimeline {
* the project's own build rather than a schedule guessed by the CLI.
*/
attachNuxt(nuxt: NuxtLike): void {
this.#splitBundles = !nuxt.options?.experimental?.viteEnvironmentApi
nuxt.hooks?.beforeEach?.(({ name, args }) => {
const phase = HOOK_PHASES[name]
if (phase) {
this.setPhase(phase, phase === 'client' && !this.#splitBundles ? 'Bundling app' : undefined)
this.setPhase(phase)
return
}
if (name === 'vite:configResolved' && this.#splitBundles) {
const ctx = args?.[1] as { isServer?: boolean } | undefined
this.setPhase(ctx?.isServer ? 'server' : 'client')
if (name === 'vite:extend') {
this.#bundledByVite = true
const { config } = args![0] as { config: { plugins?: unknown[] } }
;(config.plugins ||= []).push(this.#vitePlugin())
}
else if (name === 'nitro:build:before' && !this.#bundledByVite) {
this.setPhase('nitro')
}
else if (name === 'webpack:compile') {
const ctx = args?.[0] as { name?: string } | undefined
Expand All @@ -131,6 +136,27 @@ export class BuildProgress extends PhaseTimeline {
})
}

#vitePlugin() {
const setPhase = (id: string) => this.setPhase(id)
let ssr = false
const isServer = (ctx: VitePluginContext) => (ctx.environment?.name ?? (ssr ? 'ssr' : 'client')) === 'ssr'
return {
name: 'nuxt-cli:build-progress',
configResolved(config: { build?: { ssr?: unknown } }) {
ssr = !!config.build?.ssr
},
buildStart(this: VitePluginContext) {
setPhase(isServer(this) ? 'server' : 'client')
},
// Nitro copies public assets and prerenders before its own bundle starts.
closeBundle(this: VitePluginContext) {
if (isServer(this)) {
setPhase('nitro')
}
},
}
}

#attachNitro(hooks: HookableLike | undefined): void {
hooks?.beforeEach?.(({ name, args }) => {
// Nitro's hooks also run while Nuxt is still bundling the app, and those
Expand Down
39 changes: 32 additions & 7 deletions packages/nuxt-cli/test/unit/build-progress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,29 @@ import { BuildProgress } from '../../src/utils/build-progress'

type HookHandler = (event: { name: string, args?: unknown[] }) => void

function nuxtStub(options: { viteEnvironmentApi?: boolean } = {}) {
interface VitePlugin {
configResolved: (config: { build?: { ssr?: unknown } }) => void
buildStart: (this: { environment?: { name: string } }) => void
closeBundle: (this: { environment?: { name: string } }) => void
}

function nuxtStub() {
const handlers: HookHandler[] = []
const nitroHandlers: HookHandler[] = []
return {
nuxt: {
options: { experimental: { viteEnvironmentApi: options.viteEnvironmentApi } },
hooks: { beforeEach: (fn: HookHandler) => void handlers.push(fn) },
},
call(name: string, ...args: unknown[]) {
for (const handler of handlers) {
handler({ name, args })
}
},
extendVite() {
const config: { plugins?: VitePlugin[] } = {}
this.call('vite:extend', { config })
return config.plugins![0]!
},
nitro: { hooks: { beforeEach: (fn: HookHandler) => void nitroHandlers.push(fn) } },
callNitro(name: string, ...args: unknown[]) {
for (const handler of nitroHandlers) {
Expand All @@ -45,8 +55,13 @@ describe('build progress', () => {
stub.call('builder:generateApp')
stub.call('prepare:types')
stub.call('build:before')
stub.call('vite:configResolved', {}, { isClient: true, isServer: false })
stub.call('vite:configResolved', {}, { isClient: false, isServer: true })
const plugin = stub.extendVite()
plugin.configResolved({ build: {} })
plugin.buildStart.call({})
plugin.closeBundle.call({})
plugin.configResolved({ build: { ssr: true } })
plugin.buildStart.call({})
plugin.closeBundle.call({})
stub.call('nitro:build:before', {})
progress.finish()

Expand All @@ -62,17 +77,27 @@ describe('build progress', () => {
expect(progress.timings.map(timing => timing.phase)).toEqual(['config', 'modules', 'app', 'types', 'client', 'server', 'nitro'])
})

it('should not split the bundle phase when a single build covers both environments', () => {
it('should time each bundle when Vite builds every environment in one run', () => {
const progress = new BuildProgress()
const stub = nuxtStub({ viteEnvironmentApi: true })
const stub = nuxtStub()
progress.attachNuxt(stub.nuxt)
const seen = messages(progress)

stub.call('build:before')
const plugin = stub.extendVite()
stub.call('vite:configResolved', {}, { isClient: true, isServer: false })
stub.call('vite:configResolved', {}, { isClient: false, isServer: true })
stub.call('nitro:build:before', {})
expect(seen).toEqual(['Loading Nuxt config', 'Bundling client'])

for (const name of ['client', 'ssr', 'nitro']) {
plugin.buildStart.call({ environment: { name } })
plugin.closeBundle.call({ environment: { name } })
}
progress.finish()

expect(seen).toEqual(['Loading Nuxt config', 'Bundling app'])
expect(seen).toEqual(['Loading Nuxt config', 'Bundling client', 'Bundling server', 'Building Nitro server'])
expect(progress.timings.map(timing => timing.phase)).toEqual(['config', 'client', 'server', 'nitro'])
})

it('should name what the Nitro phase is doing', () => {
Expand Down
Loading