Skip to content

Commit 8db925e

Browse files
committed
fix(build): propagate failures and honor bare profile flag
1 parent e0a6164 commit 8db925e

2 files changed

Lines changed: 138 additions & 14 deletions

File tree

packages/nuxt-cli/src/commands/build.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ export default defineCommand({
4646
const cwd = resolveRootDir(ctx.args)
4747

4848
const profileArg = ctx.args.profile
49-
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
50-
if (profileArg) {
49+
const profiling = profileArg !== undefined
50+
const perfValue = profileArg === 'verbose' ? true : profiling ? 'quiet' : undefined
51+
if (profiling) {
5152
await startCpuProfile()
5253
}
5354

@@ -66,7 +67,6 @@ export default defineCommand({
6667
envName: ctx.args.envName, // nuxt will fall back to NODE_ENV
6768
overrides: {
6869
logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose',
69-
// TODO: remove in 3.8
7070
_generate: ctx.args.prerender,
7171
nitro: {
7272
static: ctx.args.prerender,
@@ -113,22 +113,13 @@ export default defineCommand({
113113

114114
await kit.writeTypes(nuxt)
115115

116-
nuxt.hook('build:error', async (err) => {
117-
logger.error(`Nuxt build error: ${err}`)
118-
if (profileArg) {
119-
await stopCpuProfile(cwd, 'build')
120-
}
121-
process.exit(1)
122-
})
123-
124116
await kit.buildNuxt(nuxt)
125117

126118
if (ctx.args.prerender) {
127119
if (!nuxt.options.ssr) {
128120
logger.warn(`HTML content not prerendered because ${styleText('cyan', 'ssr: false')} was set.`)
129121
logger.info(`You can read more in ${styleText('cyan', 'https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`)
130122
}
131-
// TODO: revisit later if/when nuxt build --prerender will output hybrid
132123
const dir = nitro.options.output.publicDir
133124
const publicDir = dir ? relative(process.cwd(), dir) : '.output/public'
134125
outro(`✨ You can now deploy ${styleText('cyan', publicDir)} to any static hosting! ${styleText('gray', `(${formatDuration(Date.now() - start)})`)}`)
@@ -138,10 +129,10 @@ export default defineCommand({
138129
}
139130
}
140131
finally {
141-
for (const release of releaseLocks) {
132+
for (const release of releaseLocks.reverse()) {
142133
release()
143134
}
144-
if (profileArg) {
135+
if (profiling) {
145136
await stopCpuProfile(cwd, 'build')
146137
}
147138
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { runCommand } from 'citty'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import build from '../../../src/commands/build'
5+
6+
const mocks = vi.hoisted(() => ({
7+
acquireLock: vi.fn(),
8+
acquireOutputLock: vi.fn(),
9+
buildNuxt: vi.fn(),
10+
clearBuildDir: vi.fn(),
11+
loadNuxt: vi.fn(),
12+
releaseBuildDir: vi.fn(),
13+
releaseOutputDir: vi.fn(),
14+
startCpuProfile: vi.fn(),
15+
stopCpuProfile: vi.fn(),
16+
useNitro: vi.fn(),
17+
writeTypes: vi.fn(),
18+
}))
19+
20+
vi.mock('@clack/prompts', () => ({ intro: vi.fn(), outro: vi.fn() }))
21+
vi.mock('../../../src/utils/banner', () => ({ showBanner: vi.fn() }))
22+
vi.mock('../../../src/utils/env', () => ({ overrideEnv: vi.fn() }))
23+
vi.mock('../../../src/utils/fs', () => ({ clearBuildDir: mocks.clearBuildDir }))
24+
vi.mock('../../../src/utils/kit', () => ({
25+
loadKit: () => Promise.resolve({
26+
buildNuxt: mocks.buildNuxt,
27+
loadNuxt: mocks.loadNuxt,
28+
useNitro: mocks.useNitro,
29+
writeTypes: mocks.writeTypes,
30+
}),
31+
}))
32+
vi.mock('../../../src/utils/lockfile', () => ({
33+
acquireLock: mocks.acquireLock,
34+
acquireOutputLock: mocks.acquireOutputLock,
35+
formatLockError: vi.fn(() => 'lock details'),
36+
}))
37+
vi.mock('../../../src/utils/logger', () => ({
38+
logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
39+
}))
40+
vi.mock('../../../src/utils/profile', () => ({
41+
startCpuProfile: mocks.startCpuProfile,
42+
stopCpuProfile: mocks.stopCpuProfile,
43+
}))
44+
45+
const cwd = '/project'
46+
const buildDir = '/project/.nuxt'
47+
const outputDir = '/project/.output'
48+
49+
function run(args: string[] = [], data?: Record<string, any>) {
50+
return runCommand(build, { rawArgs: [cwd, ...args], data })
51+
}
52+
53+
describe('build', () => {
54+
beforeEach(() => {
55+
vi.clearAllMocks()
56+
mocks.loadNuxt.mockResolvedValue({
57+
hook: vi.fn(),
58+
ready: vi.fn(),
59+
options: { buildDir, rootDir: cwd, ssr: true },
60+
})
61+
mocks.useNitro.mockReturnValue({
62+
options: { preset: 'node-server', output: { dir: outputDir, publicDir: `${outputDir}/public` } },
63+
})
64+
mocks.acquireLock.mockReturnValue({ release: mocks.releaseBuildDir })
65+
mocks.acquireOutputLock.mockReturnValue({ release: mocks.releaseOutputDir })
66+
})
67+
68+
it('loads Nuxt with build arguments and runs the build in order', async () => {
69+
await run(['--prerender', '--preset=cloudflare', '--extends=base', '--profile=verbose'], {
70+
overrides: { debug: { templates: true } },
71+
})
72+
73+
expect(mocks.loadNuxt).toHaveBeenCalledWith({
74+
cwd,
75+
ready: false,
76+
dotenv: { cwd, fileName: undefined },
77+
envName: undefined,
78+
overrides: {
79+
logLevel: undefined,
80+
_generate: true,
81+
nitro: { static: true, preset: 'cloudflare' },
82+
extends: 'base',
83+
debug: { templates: true, perf: true },
84+
},
85+
})
86+
expect(mocks.acquireLock).toHaveBeenCalledWith(buildDir, { command: 'build', cwd })
87+
expect(mocks.acquireOutputLock).toHaveBeenCalledWith(cwd, outputDir, { command: 'build', cwd })
88+
expect(mocks.clearBuildDir).toHaveBeenCalledWith(buildDir)
89+
expect(mocks.writeTypes).toHaveBeenCalled()
90+
expect(mocks.buildNuxt).toHaveBeenCalled()
91+
expect(mocks.startCpuProfile).toHaveBeenCalledOnce()
92+
expect(mocks.stopCpuProfile).toHaveBeenCalledWith(cwd, 'build')
93+
expect(mocks.releaseBuildDir).toHaveBeenCalledOnce()
94+
expect(mocks.releaseOutputDir).toHaveBeenCalledOnce()
95+
96+
const calls = [
97+
mocks.acquireLock,
98+
mocks.acquireOutputLock,
99+
mocks.clearBuildDir,
100+
mocks.writeTypes,
101+
mocks.buildNuxt,
102+
mocks.releaseOutputDir,
103+
mocks.releaseBuildDir,
104+
].map(mock => mock.mock.invocationCallOrder[0])
105+
expect(calls).toEqual([...calls].sort((a, b) => a! - b!))
106+
})
107+
108+
it('propagates build errors without terminating programmatic callers', async () => {
109+
const error = new Error('build failed')
110+
mocks.buildNuxt.mockRejectedValue(error)
111+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
112+
113+
await expect(run(['--profile'])).rejects.toBe(error)
114+
115+
expect(exit).not.toHaveBeenCalled()
116+
expect(mocks.startCpuProfile).toHaveBeenCalledOnce()
117+
expect(mocks.releaseBuildDir).toHaveBeenCalledOnce()
118+
expect(mocks.releaseOutputDir).toHaveBeenCalledOnce()
119+
expect(mocks.stopCpuProfile).toHaveBeenCalledWith(cwd, 'build')
120+
})
121+
122+
it('releases the build-directory lock when the output is already in use', async () => {
123+
mocks.acquireOutputLock.mockReturnValue({
124+
existing: { pid: 42, command: 'build', cwd: '/other/project', startedAt: Date.now() },
125+
})
126+
127+
await expect(run()).rejects.toThrow(/Another Nuxt build is already writing to .*\.output \(PID 42\)\./)
128+
129+
expect(mocks.clearBuildDir).not.toHaveBeenCalled()
130+
expect(mocks.buildNuxt).not.toHaveBeenCalled()
131+
expect(mocks.releaseBuildDir).toHaveBeenCalledOnce()
132+
})
133+
})

0 commit comments

Comments
 (0)