diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts index f450897a79da..09987c5324da 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -2,7 +2,7 @@ import { promises as fsp } from 'node:fs' import { join, resolve } from 'pathe' import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3' import { listen } from 'listhen' -import { writeTypes } from '../utils/prepare' +import type { NuxtAnalyzeMeta } from '@nuxt/schema' import { loadKit } from '../utils/kit' import { clearDir } from '../utils/fs' import { overrideEnv } from '../utils/env' @@ -11,62 +11,99 @@ import { defineNuxtCommand } from './index' export default defineNuxtCommand({ meta: { name: 'analyze', - usage: 'npx nuxi analyze [--log-level] [rootDir]', + usage: 'npx nuxi analyze [--log-level] [--name] [--no-serve] [rootDir]', description: 'Build nuxt and analyze production bundle (experimental)' }, async invoke (args) { overrideEnv('production') + const name = args.name || 'default' + const slug = name.trim().replace(/[^a-z0-9_-]/gi, '_') const rootDir = resolve(args._[0] || '.') - const statsDir = join(rootDir, '.nuxt/stats') + + let analyzeDir = join(rootDir, '.nuxt/analyze', slug) + let buildDir = join(analyzeDir, '.nuxt') + let outDir = join(analyzeDir, '.output') + + const startTime = Date.now() const { loadNuxt, buildNuxt } = await loadKit(rootDir) const nuxt = await loadNuxt({ rootDir, overrides: { - build: { analyze: true }, + build: { + analyze: true + }, + analyzeDir, + buildDir, + nitro: { + output: { + dir: outDir + } + }, logLevel: args['log-level'] } }) - await clearDir(nuxt.options.buildDir) - await writeTypes(nuxt) + analyzeDir = nuxt.options.analyzeDir + buildDir = nuxt.options.buildDir + outDir = nuxt.options.nitro.output?.dir || outDir + + await clearDir(analyzeDir) await buildNuxt(nuxt) - const app = createApp() + const endTime = Date.now() - const serveFile = (filePath: string) => lazyEventHandler(async () => { - const contents = await fsp.readFile(filePath, 'utf-8') - return eventHandler((event) => { event.node.res.end(contents) }) - }) + const meta: NuxtAnalyzeMeta = { + name, + slug, + startTime, + endTime, + analyzeDir, + buildDir, + outDir + } + + await nuxt.callHook('build:analyze:done', meta) + await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8') + console.info('Analyze results are available at: `' + analyzeDir + '`') console.warn('Do not deploy analyze results! Use `nuxi build` before deploying.') - console.info('Starting stats server...') - - app.use('/client', serveFile(join(statsDir, 'client.html'))) - app.use('/nitro', serveFile(join(statsDir, 'nitro.html'))) - app.use(eventHandler(() => ` - - - - Nuxt Bundle Stats (experimental) - -

Nuxt Bundle Stats (experimental)

- - - `)) - - await listen(toNodeListener(app)) - - return 'wait' as const + if (args.serve !== false && !process.env.CI) { + const app = createApp() + + const serveFile = (filePath: string) => lazyEventHandler(async () => { + const contents = await fsp.readFile(filePath, 'utf-8') + return eventHandler((event) => { event.node.res.end(contents) }) + }) + + console.info('Starting stats server...') + + app.use('/client', serveFile(join(analyzeDir, 'client.html'))) + app.use('/nitro', serveFile(join(analyzeDir, 'nitro.html'))) + app.use(eventHandler(() => ` + + + + Nuxt Bundle Stats (experimental) + +

Nuxt Bundle Stats (experimental)

+ + + `)) + + await listen(toNodeListener(app)) + + return 'wait' as const + } } }) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index dc2c2ab545ac..a582d1209d7c 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -2,7 +2,7 @@ import { relative, resolve } from 'pathe' import { consola } from 'consola' import { writeTypes } from '../utils/prepare' import { loadKit } from '../utils/kit' -import { clearDir } from '../utils/fs' +import { clearBuildDir } from '../utils/fs' import { overrideEnv } from '../utils/env' import { showVersions } from '../utils/banner' import { defineNuxtCommand } from './index' @@ -36,7 +36,7 @@ export default defineNuxtCommand({ // Use ? for backward compatibility for Nuxt <= RC.10 const nitro = useNitro?.() - await clearDir(nuxt.options.buildDir) + await clearBuildDir(nuxt.options.buildDir) await writeTypes(nuxt) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index ee527aff480e..b2c488de6783 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -12,7 +12,8 @@ import { writeTypes } from '../utils/prepare' import { loadKit } from '../utils/kit' import { importModule } from '../utils/esm' import { overrideEnv } from '../utils/env' -import { cleanupNuxtDirs, loadNuxtManifest, writeNuxtManifest } from '../utils/nuxt' +import { loadNuxtManifest, writeNuxtManifest } from '../utils/nuxt' +import { clearBuildDir } from '../utils/fs' import { defineNuxtCommand } from './index' export default defineNuxtCommand({ @@ -110,7 +111,7 @@ export default defineNuxtCommand({ const previousManifest = await loadNuxtManifest(currentNuxt.options.buildDir) const newManifest = await writeNuxtManifest(currentNuxt) if (previousManifest && newManifest && previousManifest._hash !== newManifest._hash) { - await cleanupNuxtDirs(currentNuxt.options.rootDir) + await clearBuildDir(currentNuxt.options.buildDir) } } diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index 4e194e03ebd3..43cd2f05612c 100644 --- a/packages/nuxi/src/commands/prepare.ts +++ b/packages/nuxi/src/commands/prepare.ts @@ -1,6 +1,6 @@ import { relative, resolve } from 'pathe' import { consola } from 'consola' -import { clearDir } from '../utils/fs' +import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { writeTypes } from '../utils/prepare' import { defineNuxtCommand } from './index' @@ -23,7 +23,7 @@ export default defineNuxtCommand({ logLevel: args['log-level'] } }) - await clearDir(nuxt.options.buildDir) + await clearBuildDir(nuxt.options.buildDir) await buildNuxt(nuxt) await writeTypes(nuxt) diff --git a/packages/nuxi/src/utils/fs.ts b/packages/nuxi/src/utils/fs.ts index 857cb47f2d9c..7f8640eb4fa3 100644 --- a/packages/nuxi/src/utils/fs.ts +++ b/packages/nuxi/src/utils/fs.ts @@ -1,5 +1,5 @@ -import { promises as fsp } from 'node:fs' -import { dirname } from 'pathe' +import { existsSync, promises as fsp } from 'node:fs' +import { dirname, join } from 'pathe' import { consola } from 'consola' // Check if a file exists @@ -12,11 +12,24 @@ export async function exists (path: string) { } } -export async function clearDir (path: string) { - await fsp.rm(path, { recursive: true, force: true }) +export async function clearDir (path: string, exclude?: string[]) { + if (!exclude) { + await fsp.rm(path, { recursive: true, force: true }) + } else if (existsSync(path)) { + const files = await fsp.readdir(path) + await Promise.all(files.map(async (name) => { + if (!exclude.includes(name)) { + await fsp.rm(join(path, name), { recursive: true, force: true }) + } + })) + } await fsp.mkdir(path, { recursive: true }) } +export function clearBuildDir (path: string) { + return clearDir(path, ['cache', 'analyze']) +} + export async function rmRecursive (paths: string[]) { await Promise.all(paths.filter(p => typeof p === 'string').map(async (path) => { consola.debug('Removing recursive path', path) diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 4e0e5dce9fd6..10d0ba12b60e 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -65,7 +65,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { analyze: nuxt.options.build.analyze && { template: 'treemap', projectRoot: nuxt.options.rootDir, - filename: join(nuxt.options.rootDir, '.nuxt/stats', '{name}.html') + filename: join(nuxt.options.analyzeDir, '{name}.html') }, scanDirs: nuxt.options._layers.map(layer => (layer.config.serverDir || layer.config.srcDir) && resolve(layer.cwd, layer.config.serverDir || resolve(layer.config.srcDir, 'server'))).filter(Boolean), renderer: resolve(distDir, 'core/runtime/nitro/renderer'), diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts index c905fae4ce81..cccdd038fa27 100644 --- a/packages/schema/src/config/build.ts +++ b/packages/schema/src/config/build.ts @@ -119,10 +119,11 @@ export default defineUntypedSchema({ return val ?? false } const rootDir = await get('rootDir') + const analyzeDir = await get('analyzeDir') return { template: 'treemap', projectRoot: rootDir, - filename: join(rootDir, '.nuxt/stats', '{name}.html') + filename: join(analyzeDir, '{name}.html') } } }, diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index bc0437e4545b..16ebeaeef064 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -117,20 +117,20 @@ export default defineUntypedSchema({ }, /** - * Used to set the modules directories for path resolving (for example, webpack's - * `resolveLoading`, `nodeExternals` and `postcss`). - * - * The configuration path is relative to `options.rootDir` (default is current working directory). - * - * Setting this field may be necessary if your project is organized as a yarn workspace-styled mono-repository. - * - * @example - * ```js - * export default { - * modulesDir: ['../../node_modules'] - * } - * ``` - */ + * Used to set the modules directories for path resolving (for example, webpack's + * `resolveLoading`, `nodeExternals` and `postcss`). + * + * The configuration path is relative to `options.rootDir` (default is current working directory). + * + * Setting this field may be necessary if your project is organized as a yarn workspace-styled mono-repository. + * + * @example + * ```js + * export default { + * modulesDir: ['../../node_modules'] + * } + * ``` + */ modulesDir: { $default: ['node_modules'], $resolve: async (val, get) => [ @@ -139,6 +139,17 @@ export default defineUntypedSchema({ ] }, + /** + * The directory where Nuxt will store the generated files when running `nuxt analyze`. + * + * If a relative path is specified, it will be relative to your `rootDir`. + */ + analyzeDir: { + $resolve: async (val, get) => val + ? resolve(await get('rootDir'), val) + : resolve(await get('buildDir'), 'analyze') + }, + /** * Whether Nuxt is running in development mode. * @@ -346,6 +357,7 @@ export default defineUntypedSchema({ '**/*.d.ts', // ignore type declarations '.output', '.git', + await get('analyzeDir'), await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*` ].concat(val).filter(Boolean) }, diff --git a/packages/schema/src/config/webpack.ts b/packages/schema/src/config/webpack.ts index edd3cf468949..6675d44a5675 100644 --- a/packages/schema/src/config/webpack.ts +++ b/packages/schema/src/config/webpack.ts @@ -22,10 +22,11 @@ export default defineUntypedSchema({ return val ?? false } const rootDir = await get('rootDir') + const analyzeDir = await get('analyzeDir') return { template: 'treemap', projectRoot: rootDir, - filename: join(rootDir, '.nuxt/stats', '{name}.html') + filename: join(analyzeDir, '{name}.html') } } }, diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts index b11ed93fa63e..e3143369757f 100644 --- a/packages/schema/src/types/hooks.ts +++ b/packages/schema/src/types/hooks.ts @@ -47,6 +47,16 @@ export interface GenerateAppOptions { filter?: (template: ResolvedNuxtTemplate) => boolean } +export interface NuxtAnalyzeMeta { + name: string + slug: string + startTime: number + endTime: number + analyzeDir: string + buildDir: string + outDir: string +} + /** * The listeners to Nuxt build time events */ @@ -131,6 +141,13 @@ export interface NuxtHooks { */ 'build:manifest': (manifest: Manifest) => HookResult + /** + * Called when `nuxt analyze` is finished + * @param meta the analyze meta object, mutations will be saved to `meta.json` + * @returns Promise + */ + 'build:analyze:done': (meta: NuxtAnalyzeMeta) => HookResult + /** * Called before generating the app. * @param options GenerateAppOptions object