Skip to content

Commit 89c0f04

Browse files
committed
Introduce next analyze: a built-in bundle analyzer for Turbopack
1 parent 52469f6 commit 89c0f04

File tree

8 files changed

+469
-53
lines changed

8 files changed

+469
-53
lines changed

packages/next/src/bin/next.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { NextTelemetryOptions } from '../cli/next-telemetry.js'
2727
import type { NextStartOptions } from '../cli/next-start.js'
2828
import type { NextInfoOptions } from '../cli/next-info.js'
2929
import type { NextDevOptions } from '../cli/next-dev.js'
30+
import type { NextAnalyzeOptions } from '../cli/next-analyze.js'
3031
import type { NextBuildOptions } from '../cli/next-build.js'
3132
import type { NextTypegenOptions } from '../cli/next-typegen.js'
3233

@@ -198,6 +199,34 @@ program
198199
})
199200
.usage('[directory] [options]')
200201

202+
program
203+
.command('experimental-analyze')
204+
.description(
205+
'Analyze bundle output. Does not produce build artifacts. Only compatible with Turbopack.'
206+
)
207+
.argument(
208+
'[directory]',
209+
`A directory on which to analyze the application. ${italic(
210+
'If no directory is provided, the current directory will be used.'
211+
)}`
212+
)
213+
.addOption(
214+
new Option(
215+
'--experimental-build-mode [mode]',
216+
'Uses an experimental build mode.'
217+
)
218+
.choices(['compile', 'generate', 'generate-env'])
219+
.default('default')
220+
)
221+
.option('--no-mangling', 'Disables mangling.')
222+
.option('--profile', 'Enables production profiling for React.')
223+
.option('--open', 'Open the bundle analyzer in a browser after analysis.')
224+
.action((directory: string, options: NextAnalyzeOptions) => {
225+
return import('../cli/next-analyze.js').then((mod) =>
226+
mod.nextAnalyze(options, directory).then(() => process.exit(0))
227+
)
228+
})
229+
201230
program
202231
.command('dev', { isDefault: true })
203232
.description(
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import type { NextConfigComplete } from '../../server/config-shared'
2+
import type { __ApiPreviewProps } from '../../server/api-utils'
3+
4+
import { trace, setGlobal } from '../../trace'
5+
import * as Log from '../output/log'
6+
import * as path from 'node:path'
7+
import loadConfig from '../../server/config'
8+
import { PHASE_ANALYZE } from '../../shared/lib/constants'
9+
import { turbopackAnalyze, type AnalyzeContext } from '../turbopack-analyze'
10+
import { durationToString } from '../duration-to-string'
11+
import { cp, writeFile } from 'node:fs/promises'
12+
import {
13+
collectAppFiles,
14+
collectPagesFiles,
15+
createPagesMapping,
16+
processLayoutRoutes,
17+
} from '../entries'
18+
import { createValidFileMatcher } from '../../server/lib/find-page-file'
19+
import { findPagesDir } from '../../lib/find-pages-dir'
20+
import { PAGE_TYPES } from '../../lib/page-types'
21+
import { pageToRoute } from '../utils'
22+
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
23+
24+
export default async function analyze(
25+
dir: string,
26+
reactProductionProfiling = false,
27+
noMangling = false,
28+
appDirOnly = false,
29+
experimentalBuildMode: 'default' | 'compile' | 'generate' | 'generate-env'
30+
): Promise<void> {
31+
console.log(
32+
'Analyze args dir:',
33+
dir,
34+
'reactProductionProfiling:',
35+
reactProductionProfiling,
36+
'noMangling:',
37+
noMangling,
38+
'appDirOnly:',
39+
appDirOnly,
40+
'experimentalBuildMode:',
41+
experimentalBuildMode
42+
)
43+
// const analyzeStartTime = Date.now()
44+
// const telemetry = traceGlobals.get('telemetry') as Telemetry
45+
try {
46+
const nextAnalyzeSpan = trace('next-build', undefined, {
47+
buildMode: experimentalBuildMode,
48+
version: process.env.__NEXT_VERSION as string,
49+
})
50+
51+
await nextAnalyzeSpan.traceAsyncFn(async () => {
52+
const config: NextConfigComplete = await nextAnalyzeSpan
53+
.traceChild('load-next-config')
54+
.traceAsyncFn(() =>
55+
loadConfig(PHASE_ANALYZE, dir, {
56+
// Log for next.config loading process
57+
silent: false,
58+
reactProductionProfiling,
59+
})
60+
)
61+
62+
// Reading the config can modify environment variables that influence the bundler selection.
63+
nextAnalyzeSpan.setAttribute('bundler', 'turbopack')
64+
65+
process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || ''
66+
67+
const distDir = path.join(dir, '.next')
68+
setGlobal('phase', PHASE_ANALYZE)
69+
setGlobal('distDir', distDir)
70+
// await nextAnalyzeSpan
71+
// .traceChild('telemetry-flush')
72+
// .traceAsyncFn(() => telemetry.flush())
73+
74+
Log.info('Creating an optimized production build ...')
75+
76+
const analyzeContext: AnalyzeContext = {
77+
config,
78+
dir,
79+
distDir,
80+
noMangling,
81+
appDirOnly,
82+
}
83+
84+
console.log('analyzeContext:', analyzeContext)
85+
86+
let shutdownPromise = Promise.resolve()
87+
const { duration: analyzeDuration, shutdownPromise: p } =
88+
await turbopackAnalyze(analyzeContext)
89+
shutdownPromise = p
90+
91+
const durationString = durationToString(analyzeDuration)
92+
Log.event(`Compiled successfully in ${durationString}`)
93+
94+
// telemetry.record(
95+
// eventBuildCompleted(pagesPaths, {
96+
// bundler: 'turbopack',
97+
// durationInSeconds: Math.round(compilerDuration),
98+
// totalAppPagesCount,
99+
// })
100+
// )
101+
102+
await shutdownPromise
103+
104+
await cp(
105+
path.join(__dirname, '../../bundle-analyzer'),
106+
path.join(dir, '.next/diagnostics/analyze'),
107+
{ recursive: true }
108+
)
109+
110+
const routes = await collectRoutes(dir, appDirOnly, config)
111+
112+
// Write an index of routes for the route picker
113+
await writeFile(
114+
path.join(dir, '.next/diagnostics/analyze/data/routes.json'),
115+
JSON.stringify(
116+
routes.appRoutes
117+
.concat(routes.layoutRoutes)
118+
.concat(routes.pagesRoutes ?? [])
119+
.filter((r) => !r.includes('/node_modules/')),
120+
null,
121+
2
122+
)
123+
)
124+
})
125+
} catch (e) {
126+
// const telemetry: Telemetry | undefined = traceGlobals.get('telemetry')
127+
// if (telemetry) {
128+
// telemetry.record(
129+
// eventBuildFailed({
130+
// bundler: 'turbopack',
131+
// errorCode: getErrorCodeForTelemetry(e),
132+
// durationInSeconds: Math.floor((Date.now() - analyzeStartTime) / 1000),
133+
// })
134+
// )
135+
// }
136+
throw e
137+
} finally {
138+
// await flushAllTraces()
139+
}
140+
}
141+
142+
async function collectRoutes(
143+
dir: string,
144+
appDirOnly: boolean,
145+
config: NextConfigComplete
146+
): Promise<{
147+
appRoutes: string[]
148+
layoutRoutes: string[]
149+
pagesRoutes: string[] | null
150+
}> {
151+
const { pagesDir, appDir } = findPagesDir(dir)
152+
153+
const validFileMatcher = createValidFileMatcher(config.pageExtensions, appDir)
154+
155+
const { appPaths, layoutPaths } = await collectAppFiles(dir, validFileMatcher)
156+
157+
const pagesPaths = pagesDir
158+
? await collectPagesFiles(pagesDir, validFileMatcher)
159+
: null
160+
161+
const appMapping = await createPagesMapping({
162+
pagePaths: appPaths,
163+
isDev: false,
164+
pagesType: PAGE_TYPES.APP,
165+
pageExtensions: config.pageExtensions,
166+
pagesDir,
167+
appDir,
168+
appDirOnly,
169+
})
170+
171+
console.log('appMapping:', appMapping)
172+
173+
const layoutsMapping = await createPagesMapping({
174+
pagePaths: layoutPaths,
175+
isDev: false,
176+
pagesType: PAGE_TYPES.APP,
177+
pageExtensions: config.pageExtensions,
178+
pagesDir,
179+
appDir,
180+
appDirOnly,
181+
})
182+
183+
const pagesMapping = pagesPaths
184+
? await createPagesMapping({
185+
pagePaths: pagesPaths,
186+
isDev: false,
187+
pagesType: PAGE_TYPES.PAGES,
188+
pageExtensions: config.pageExtensions,
189+
pagesDir,
190+
appDir,
191+
appDirOnly,
192+
})
193+
: null
194+
195+
return {
196+
appRoutes: Object.keys(appMapping)
197+
.map(pageToRouteName)
198+
.map(normalizeAppPath),
199+
layoutRoutes: processLayoutRoutes(layoutsMapping, dir, false).map(
200+
(r) => r.route
201+
),
202+
pagesRoutes: pagesMapping
203+
? Object.keys(pagesMapping).map(pageToRouteName)
204+
: null,
205+
}
206+
}
207+
208+
function pageToRouteName(page: string): string {
209+
console.log('pageToRouteName page:', page, 'route:', pageToRoute(page))
210+
return pageToRoute(page).page
211+
}

packages/next/src/build/index.ts

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ import {
140140
collectRoutesUsingEdgeRuntime,
141141
collectMeta,
142142
isProxyFile,
143+
pageToRoute,
143144
} from './utils'
144-
import type { PageInfo, PageInfos } from './utils'
145+
import type { DynamicManifestRoute, PageInfo, PageInfos } from './utils'
145146
import type { FallbackRouteParam, PrerenderedRoute } from './static-paths/types'
146147
import type { AppSegmentConfig } from './segment-config/app/app-segment-config'
147148
import { writeBuildId } from './write-build-id'
@@ -415,15 +416,6 @@ export type ManifestRoute = ManifestBuiltRoute & {
415416
skipInternalRouting?: boolean
416417
}
417418

418-
type DynamicManifestRoute = ManifestRoute & {
419-
/**
420-
* The source page that this route is based on. This is used to determine the
421-
* source page for the route and is only relevant for app pages where PPR is
422-
* enabled and the page differs from the source page.
423-
*/
424-
sourcePage: string | undefined
425-
}
426-
427419
type ManifestDataRoute = {
428420
page: string
429421
routeKeys?: { [key: string]: string }
@@ -505,42 +497,6 @@ export type RoutesManifest = {
505497
}
506498
}
507499

508-
/**
509-
* Converts a page to a manifest route.
510-
*
511-
* @param page The page to convert to a route.
512-
* @returns A route object.
513-
*/
514-
function pageToRoute(page: string): ManifestRoute
515-
/**
516-
* Converts a page to a dynamic manifest route.
517-
*
518-
* @param page The page to convert to a route.
519-
* @param sourcePage The source page that this route is based on. This is used
520-
* to determine the source page for the route and is only relevant for app
521-
* pages when PPR is enabled on them.
522-
* @returns A route object.
523-
*/
524-
function pageToRoute(
525-
page: string,
526-
sourcePage: string | undefined
527-
): DynamicManifestRoute
528-
function pageToRoute(
529-
page: string,
530-
sourcePage?: string
531-
): DynamicManifestRoute | ManifestRoute {
532-
const routeRegex = getNamedRouteRegex(page, {
533-
prefixRouteKeys: true,
534-
})
535-
return {
536-
sourcePage,
537-
page,
538-
regex: normalizeRouteRegex(routeRegex.re.source),
539-
routeKeys: routeRegex.routeKeys,
540-
namedRegex: routeRegex.namedRegex,
541-
}
542-
}
543-
544500
function getCacheDir(distDir: string): string {
545501
const cacheDir = path.join(distDir, 'cache')
546502
if (ciEnvironment.isCI && !ciEnvironment.hasNextSupport) {

0 commit comments

Comments
 (0)