Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxi): allow greater control of nuxi analyze from cli #20387

Merged
merged 10 commits into from May 2, 2023
109 changes: 73 additions & 36 deletions packages/nuxi/src/commands/analyze.ts
Expand Up @@ -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'
Expand All @@ -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]',
danielroe marked this conversation as resolved.
Show resolved Hide resolved
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(() => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nuxt Bundle Stats (experimental)</title>
</head>
<h1>Nuxt Bundle Stats (experimental)</h1>
<ul>
<li>
<a href="/nitro">Nitro server bundle stats</a>
</li>
<li>
<a href="/client">Client bundle stats</a>
</li>
</ul>
</html>
`))

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(() => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nuxt Bundle Stats (experimental)</title>
</head>
<h1>Nuxt Bundle Stats (experimental)</h1>
<ul>
<li>
<a href="/nitro">Nitro server bundle stats</a>
</li>
<li>
<a href="/client">Client bundle stats</a>
</li>
</ul>
</html>
`))

await listen(toNodeListener(app))

return 'wait' as const
}
}
})
4 changes: 2 additions & 2 deletions packages/nuxi/src/commands/build.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 3 additions & 2 deletions packages/nuxi/src/commands/dev.ts
Expand Up @@ -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({
Expand Down Expand Up @@ -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)
}
}

Expand Down
4 changes: 2 additions & 2 deletions 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'
Expand All @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions 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
Expand All @@ -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'])
pi0 marked this conversation as resolved.
Show resolved Hide resolved
}

export async function rmRecursive (paths: string[]) {
await Promise.all(paths.filter(p => typeof p === 'string').map(async (path) => {
consola.debug('Removing recursive path', path)
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/core/nitro.ts
Expand Up @@ -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'),
Expand Down
3 changes: 2 additions & 1 deletion packages/schema/src/config/build.ts
Expand Up @@ -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')
}
}
},
Expand Down
40 changes: 26 additions & 14 deletions packages/schema/src/config/common.ts
Expand Up @@ -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) => [
Expand All @@ -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.
*
Expand Down Expand Up @@ -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)
},
Expand Down
3 changes: 2 additions & 1 deletion packages/schema/src/config/webpack.ts
Expand Up @@ -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')
}
}
},
Expand Down
17 changes: 17 additions & 0 deletions packages/schema/src/types/hooks.ts
Expand Up @@ -47,6 +47,16 @@ export interface GenerateAppOptions {
filter?: (template: ResolvedNuxtTemplate<any>) => 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
*/
Expand Down Expand Up @@ -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
Expand Down