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

This commit is contained in:
Anthony Fu 2023-05-02 15:24:11 +02:00 committed by GitHub
parent 84559e84aa
commit fb76b3931a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 142 additions and 60 deletions

View File

@ -2,7 +2,7 @@ import { promises as fsp } from 'node:fs'
import { join, resolve } from 'pathe' import { join, resolve } from 'pathe'
import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3' import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3'
import { listen } from 'listhen' import { listen } from 'listhen'
import { writeTypes } from '../utils/prepare' import type { NuxtAnalyzeMeta } from '@nuxt/schema'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { clearDir } from '../utils/fs' import { clearDir } from '../utils/fs'
import { overrideEnv } from '../utils/env' import { overrideEnv } from '../utils/env'
@ -11,62 +11,99 @@ import { defineNuxtCommand } from './index'
export default defineNuxtCommand({ export default defineNuxtCommand({
meta: { meta: {
name: 'analyze', 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)' description: 'Build nuxt and analyze production bundle (experimental)'
}, },
async invoke (args) { async invoke (args) {
overrideEnv('production') overrideEnv('production')
const name = args.name || 'default'
const slug = name.trim().replace(/[^a-z0-9_-]/gi, '_')
const rootDir = resolve(args._[0] || '.') 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 { loadNuxt, buildNuxt } = await loadKit(rootDir)
const nuxt = await loadNuxt({ const nuxt = await loadNuxt({
rootDir, rootDir,
overrides: { overrides: {
build: { analyze: true }, build: {
analyze: true
},
analyzeDir,
buildDir,
nitro: {
output: {
dir: outDir
}
},
logLevel: args['log-level'] logLevel: args['log-level']
} }
}) })
await clearDir(nuxt.options.buildDir) analyzeDir = nuxt.options.analyzeDir
await writeTypes(nuxt) buildDir = nuxt.options.buildDir
outDir = nuxt.options.nitro.output?.dir || outDir
await clearDir(analyzeDir)
await buildNuxt(nuxt) await buildNuxt(nuxt)
const app = createApp() const endTime = Date.now()
const serveFile = (filePath: string) => lazyEventHandler(async () => { const meta: NuxtAnalyzeMeta = {
const contents = await fsp.readFile(filePath, 'utf-8') name,
return eventHandler((event) => { event.node.res.end(contents) }) 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.warn('Do not deploy analyze results! Use `nuxi build` before deploying.')
console.info('Starting stats server...') if (args.serve !== false && !process.env.CI) {
const app = createApp()
app.use('/client', serveFile(join(statsDir, 'client.html'))) const serveFile = (filePath: string) => lazyEventHandler(async () => {
app.use('/nitro', serveFile(join(statsDir, 'nitro.html'))) const contents = await fsp.readFile(filePath, 'utf-8')
app.use(eventHandler(() => `<!DOCTYPE html> return eventHandler((event) => { event.node.res.end(contents) })
<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)) console.info('Starting stats server...')
return 'wait' as const 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
}
} }
}) })

View File

@ -2,7 +2,7 @@ import { relative, resolve } from 'pathe'
import { consola } from 'consola' import { consola } from 'consola'
import { writeTypes } from '../utils/prepare' import { writeTypes } from '../utils/prepare'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { clearDir } from '../utils/fs' import { clearBuildDir } from '../utils/fs'
import { overrideEnv } from '../utils/env' import { overrideEnv } from '../utils/env'
import { showVersions } from '../utils/banner' import { showVersions } from '../utils/banner'
import { defineNuxtCommand } from './index' import { defineNuxtCommand } from './index'
@ -36,7 +36,7 @@ export default defineNuxtCommand({
// Use ? for backward compatibility for Nuxt <= RC.10 // Use ? for backward compatibility for Nuxt <= RC.10
const nitro = useNitro?.() const nitro = useNitro?.()
await clearDir(nuxt.options.buildDir) await clearBuildDir(nuxt.options.buildDir)
await writeTypes(nuxt) await writeTypes(nuxt)

View File

@ -12,7 +12,8 @@ import { writeTypes } from '../utils/prepare'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { importModule } from '../utils/esm' import { importModule } from '../utils/esm'
import { overrideEnv } from '../utils/env' 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' import { defineNuxtCommand } from './index'
export default defineNuxtCommand({ export default defineNuxtCommand({
@ -110,7 +111,7 @@ export default defineNuxtCommand({
const previousManifest = await loadNuxtManifest(currentNuxt.options.buildDir) const previousManifest = await loadNuxtManifest(currentNuxt.options.buildDir)
const newManifest = await writeNuxtManifest(currentNuxt) const newManifest = await writeNuxtManifest(currentNuxt)
if (previousManifest && newManifest && previousManifest._hash !== newManifest._hash) { if (previousManifest && newManifest && previousManifest._hash !== newManifest._hash) {
await cleanupNuxtDirs(currentNuxt.options.rootDir) await clearBuildDir(currentNuxt.options.buildDir)
} }
} }

View File

@ -1,6 +1,6 @@
import { relative, resolve } from 'pathe' import { relative, resolve } from 'pathe'
import { consola } from 'consola' import { consola } from 'consola'
import { clearDir } from '../utils/fs' import { clearBuildDir } from '../utils/fs'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare' import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index' import { defineNuxtCommand } from './index'
@ -23,7 +23,7 @@ export default defineNuxtCommand({
logLevel: args['log-level'] logLevel: args['log-level']
} }
}) })
await clearDir(nuxt.options.buildDir) await clearBuildDir(nuxt.options.buildDir)
await buildNuxt(nuxt) await buildNuxt(nuxt)
await writeTypes(nuxt) await writeTypes(nuxt)

View File

@ -1,5 +1,5 @@
import { promises as fsp } from 'node:fs' import { existsSync, promises as fsp } from 'node:fs'
import { dirname } from 'pathe' import { dirname, join } from 'pathe'
import { consola } from 'consola' import { consola } from 'consola'
// Check if a file exists // Check if a file exists
@ -12,11 +12,24 @@ export async function exists (path: string) {
} }
} }
export async function clearDir (path: string) { export async function clearDir (path: string, exclude?: string[]) {
await fsp.rm(path, { recursive: true, force: true }) 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 }) await fsp.mkdir(path, { recursive: true })
} }
export function clearBuildDir (path: string) {
return clearDir(path, ['cache', 'analyze'])
}
export async function rmRecursive (paths: string[]) { export async function rmRecursive (paths: string[]) {
await Promise.all(paths.filter(p => typeof p === 'string').map(async (path) => { await Promise.all(paths.filter(p => typeof p === 'string').map(async (path) => {
consola.debug('Removing recursive path', path) consola.debug('Removing recursive path', path)

View File

@ -65,7 +65,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
analyze: nuxt.options.build.analyze && { analyze: nuxt.options.build.analyze && {
template: 'treemap', template: 'treemap',
projectRoot: nuxt.options.rootDir, 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), 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'), renderer: resolve(distDir, 'core/runtime/nitro/renderer'),

View File

@ -119,10 +119,11 @@ export default defineUntypedSchema({
return val ?? false return val ?? false
} }
const rootDir = await get('rootDir') const rootDir = await get('rootDir')
const analyzeDir = await get('analyzeDir')
return { return {
template: 'treemap', template: 'treemap',
projectRoot: rootDir, projectRoot: rootDir,
filename: join(rootDir, '.nuxt/stats', '{name}.html') filename: join(analyzeDir, '{name}.html')
} }
} }
}, },

View File

@ -117,20 +117,20 @@ export default defineUntypedSchema({
}, },
/** /**
* Used to set the modules directories for path resolving (for example, webpack's * Used to set the modules directories for path resolving (for example, webpack's
* `resolveLoading`, `nodeExternals` and `postcss`). * `resolveLoading`, `nodeExternals` and `postcss`).
* *
* The configuration path is relative to `options.rootDir` (default is current working directory). * 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. * Setting this field may be necessary if your project is organized as a yarn workspace-styled mono-repository.
* *
* @example * @example
* ```js * ```js
* export default { * export default {
* modulesDir: ['../../node_modules'] * modulesDir: ['../../node_modules']
* } * }
* ``` * ```
*/ */
modulesDir: { modulesDir: {
$default: ['node_modules'], $default: ['node_modules'],
$resolve: async (val, get) => [ $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. * Whether Nuxt is running in development mode.
* *
@ -346,6 +357,7 @@ export default defineUntypedSchema({
'**/*.d.ts', // ignore type declarations '**/*.d.ts', // ignore type declarations
'.output', '.output',
'.git', '.git',
await get('analyzeDir'),
await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*` await get('ignorePrefix') && `**/${await get('ignorePrefix')}*.*`
].concat(val).filter(Boolean) ].concat(val).filter(Boolean)
}, },

View File

@ -22,10 +22,11 @@ export default defineUntypedSchema({
return val ?? false return val ?? false
} }
const rootDir = await get('rootDir') const rootDir = await get('rootDir')
const analyzeDir = await get('analyzeDir')
return { return {
template: 'treemap', template: 'treemap',
projectRoot: rootDir, projectRoot: rootDir,
filename: join(rootDir, '.nuxt/stats', '{name}.html') filename: join(analyzeDir, '{name}.html')
} }
} }
}, },

View File

@ -47,6 +47,16 @@ export interface GenerateAppOptions {
filter?: (template: ResolvedNuxtTemplate<any>) => boolean 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 * The listeners to Nuxt build time events
*/ */
@ -131,6 +141,13 @@ export interface NuxtHooks {
*/ */
'build:manifest': (manifest: Manifest) => HookResult '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. * Called before generating the app.
* @param options GenerateAppOptions object * @param options GenerateAppOptions object