@@ -7,13 +7,13 @@ import { styleText } from 'node:util'
77import { intro, note, outro, taskLog } from '@clack/prompts'
88import { defineCommand } from 'citty'
99import { defu } from 'defu'
10- import { H3, lazyEventHandler } from 'h3-next'
11- import { join } from 'pathe'
10+ import { join, relative, resolve } from 'pathe'
1211import { serve } from 'srvx'
1312
1413import { overrideEnv } from '../utils/env'
1514import { clearDir } from '../utils/fs'
1615import { loadKit } from '../utils/kit'
16+ import { acquireLock, acquireOutputLock, formatLockError } from '../utils/lockfile'
1717import { logger } from '../utils/logger'
1818import { relativeToProcess, resolveRootDir } from '../utils/paths'
1919import { dotEnvArgs, extendsArgs, logLevelArgs, rootDirArgs } from './_shared'
@@ -72,7 +72,7 @@ export default defineCommand({
7272
7373 const cwd = resolveRootDir(ctx.args)
7474 const name = ctx.args.name || 'default'
75- const slug = name.trim().replace(NON_WORD_RE, '_')
75+ const slug = name.trim().replace(NON_WORD_RE, '_') || 'default'
7676
7777 intro(styleText('cyan', 'Analyzing bundle size...'))
7878
@@ -131,62 +131,86 @@ export default defineCommand({
131131
132132 const analyzeDir = nuxt.options.analyzeDir
133133 const buildDir = nuxt.options.buildDir
134- const outDir
135- = nuxt.options.nitro.output?.dir || join(nuxt.options.rootDir, '.output')
134+ const outDir = resolve(nuxt.options.rootDir, nuxt.options.nitro.output?.dir || '.output')
136135
137136 nuxt.options.build.analyze = defu(nuxt.options.build.analyze, {
138137 filename: join(analyzeDir, 'client.html'),
139138 })
140139
141- const tasklog = taskLog({
142- title: 'Building Nuxt with analysis enabled',
143- retainLog: false,
144- limit: 1,
145- })
146-
147- tasklog.message('Clearing analyze directory...')
148- await clearDir(analyzeDir)
149- tasklog.message('Building Nuxt...')
150- await buildNuxt(nuxt)
151- tasklog.success('Build complete')
140+ const lockInfo = { command: 'analyze' as const, cwd }
141+ const lock = acquireLock(buildDir, lockInfo)
142+ if (lock.existing) {
143+ logger.error(formatLockError(lock.existing))
144+ throw new Error(`Another Nuxt ${lock.existing.command} is already running (PID ${lock.existing.pid}).`)
145+ }
152146
153- if (skippedPrerenderRoutes > 0) {
154- logger.info(`Skipped prerendering ${skippedPrerenderRoutes} route${skippedPrerenderRoutes === 1 ? '' : 's'}. Pass ${styleText('cyan', '--prerender')} to include assets emitted while prerendering.`)
147+ const outputLock = acquireOutputLock(nuxt.options.rootDir, outDir, lockInfo)
148+ if (outputLock.existing) {
149+ lock.release()
150+ logger.error(formatLockError(outputLock.existing))
151+ throw new Error(`Another Nuxt build is already writing to ${relative(process.cwd(), outDir)} (PID ${outputLock.existing.pid}).`)
155152 }
156153
157- const endTime = Date.now()
154+ try {
155+ const tasklog = taskLog({
156+ title: 'Building Nuxt with analysis enabled',
157+ retainLog: false,
158+ limit: 1,
159+ })
158160
159- const meta: NuxtAnalyzeMeta = {
160- name,
161- slug,
162- startTime,
163- endTime,
164- analyzeDir,
165- buildDir,
166- outDir,
161+ tasklog.message('Clearing analyze directory...')
162+ await clearDir(analyzeDir)
163+ tasklog.message('Building Nuxt...')
164+ await buildNuxt(nuxt)
165+ tasklog.success('Build complete')
166+
167+ if (skippedPrerenderRoutes > 0) {
168+ logger.info(`Skipped prerendering ${skippedPrerenderRoutes} route${skippedPrerenderRoutes === 1 ? '' : 's'}. Pass ${styleText('cyan', '--prerender')} to include assets emitted while prerendering.`)
169+ }
170+
171+ const meta: NuxtAnalyzeMeta = {
172+ name,
173+ slug,
174+ startTime,
175+ endTime: Date.now(),
176+ analyzeDir,
177+ buildDir,
178+ outDir,
179+ }
180+
181+ await nuxt.callHook('build:analyze:done', meta)
182+ await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8')
183+ }
184+ finally {
185+ outputLock.release()
186+ lock.release()
167187 }
168-
169- await nuxt.callHook('build:analyze:done', meta)
170- await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8')
171188
172189 note(`${relativeToProcess(analyzeDir)}\n\nDo not deploy analyze results! Use ${styleText('cyan', 'nuxt build')} before deploying.`, 'Build location')
173190
174191 if (ctx.args.serve !== false && !process.env.CI) {
175- const app = new H3()
176-
177- const opts = { headers: { 'content-type': 'text/html' } }
178- const serveFile = (filePath: string) => lazyEventHandler(async () => {
179- const contents = await fsp.readFile(filePath, 'utf-8')
180- return () => new Response(contents, opts)
181- })
192+ const headers = { 'content-type': 'text/html' }
193+ const readReport = (name: string) => fsp.readFile(join(analyzeDir, name), 'utf8').catch(() => undefined)
194+ const reports = new Map([
195+ ['/client', await readReport('client.html')],
196+ ['/nitro', await readReport('nitro.html')],
197+ ])
182198
183199 logger.step('Starting stats server...')
184200
185- app.use('/client', serveFile(join(analyzeDir, 'client.html')))
186- app.use('/nitro', serveFile(join(analyzeDir, 'nitro.html')))
187- app.use(() => new Response(indexHtml, opts))
188-
189- await serve(app).serve()
201+ await serve({
202+ hostname: process.env.HOST || 'localhost',
203+ fetch(request) {
204+ const pathname = new URL(request.url).pathname.replace(/\/$/, '')
205+ if (reports.has(pathname)) {
206+ const report = reports.get(pathname)
207+ return report === undefined
208+ ? new Response('This report was not generated by the analyze build.', { status: 404, headers })
209+ : new Response(report, { headers })
210+ }
211+ return new Response(indexHtml, { headers })
212+ },
213+ }).ready()
190214 }
191215 else {
192216 outro('✨ Analysis build complete!')
0 commit comments