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
constcwd=resolveRootDir(ctx.args)
4747

4848
constprofileArg=ctx.args.profile
49-
constperfValue=profileArg==='verbose' ? true : profileArg ? 'quiet' : undefined
50-
if(profileArg){
49+
constprofiling=profileArg!==undefined
50+
constperfValue=profileArg==='verbose' ? true : profiling ? 'quiet' : undefined
51+
if(profiling){
5152
awaitstartCpuProfile()
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.logLevelas'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
awaitkit.writeTypes(nuxt)
115115

116-
nuxt.hook('build:error',async(err)=>{
117-
logger.error(`Nuxt build error: ${err}`)
118-
if(profileArg){
119-
awaitstopCpuProfile(cwd,'build')
120-
}
121-
process.exit(1)
122-
})
123-
124116
awaitkit.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
constdir=nitro.options.output.publicDir
133124
constpublicDir=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(constreleaseofreleaseLocks){
132+
for(constreleaseofreleaseLocks.reverse()){
142133
release()
143134
}
144-
if(profileArg){
135+
if(profiling){
145136
awaitstopCpuProfile(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+
importbuildfrom'../../../src/commands/build'
5+
6+
constmocks=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+
constcwd='/project'
46+
constbuildDir='/project/.nuxt'
47+
constoutputDir='/project/.output'
48+
49+
functionrun(args: string[]=[],data?: Record<string,any>){
50+
returnrunCommand(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+
awaitrun(['--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+
constcalls=[
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+
consterror=newError('build failed')
110+
mocks.buildNuxt.mockRejectedValue(error)
111+
constexit=vi.spyOn(process,'exit').mockImplementation(()=>undefinedasnever)
112+
113+
awaitexpect(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+
awaitexpect(run()).rejects.toThrow(/AnotherNuxtbuildisalreadywritingto.*\.output\(PID42\)\./)
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)