Skip to content

Commit

Permalink
Add test to assert on current build output tree view (#76342)
Browse files Browse the repository at this point in the history
This allows us to better track fixes and changes to the build output tree view.

To avoid flakiness, and needing to update the snapshots frequently, I've stubbed to file sizes and omitted the chunks from the snapshotted output. Because those details affect the column sizes, we can't just blank them out when reading the CLI output (which was [my first approach](d7ee6b4)). So instead, we already stub them during the generation of the output if `__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT` is defined.
  • Loading branch information
unstubbable authored Feb 23, 2025
1 parent b84bd52 commit 3d065cf
Show file tree
Hide file tree
Showing 20 changed files with 246 additions and 13 deletions.
43 changes: 31 additions & 12 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,22 @@ export async function printTreeView(
gzipSize?: boolean
}
) {
const getPrettySize = (_size: number): string => {
const size = prettyBytes(_size)
return white(bold(size))
const getPrettySize = (
_size: number,
{ strong }: { strong?: boolean } = {}
): string => {
const size = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
? 'N/A kB'
: prettyBytes(_size)

return strong ? white(bold(size)) : size
}

const MIN_DURATION = 300
// Can be overridden for test purposes to omit the build duration output.
const MIN_DURATION = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
? Infinity // Don't ever log build durations.
: 300

const getPrettyDuration = (_duration: number): string => {
const duration = `${_duration} ms`
// green for 300-1000ms
Expand Down Expand Up @@ -526,14 +536,14 @@ export async function printTreeView(
? ampFirst
? cyan('AMP')
: pageInfo.size >= 0
? prettyBytes(pageInfo.size)
? getPrettySize(pageInfo.size)
: ''
: '',
pageInfo
? ampFirst
? cyan('AMP')
: pageInfo.size >= 0
? getPrettySize(pageInfo.totalSize)
? getPrettySize(pageInfo.totalSize, { strong: true })
: ''
: '',
])
Expand All @@ -553,7 +563,7 @@ export async function printTreeView(
const size = stats.sizes.get(file)
messages.push([
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
typeof size === 'number' ? prettyBytes(size) : '',
typeof size === 'number' ? getPrettySize(size) : '',
'',
])
})
Expand Down Expand Up @@ -628,11 +638,16 @@ export async function printTreeView(
})

const sharedFilesSize = stats.router[routerType]?.common.size.total
const sharedFiles = stats.router[routerType]?.common.files ?? []

const sharedFiles = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
? []
: stats.router[routerType]?.common.files ?? []

messages.push([
'+ First Load JS shared by all',
typeof sharedFilesSize === 'number' ? getPrettySize(sharedFilesSize) : '',
typeof sharedFilesSize === 'number'
? getPrettySize(sharedFilesSize, { strong: true })
: '',
'',
])
const sharedCssFiles: string[] = []
Expand Down Expand Up @@ -667,13 +682,13 @@ export async function printTreeView(
return
}

messages.push([` ${innerSymbol} ${cleanName}`, prettyBytes(size), ''])
messages.push([` ${innerSymbol} ${cleanName}`, getPrettySize(size), ''])
})

if (restChunkCount > 0) {
messages.push([
` └ other shared chunks (total)`,
prettyBytes(restChunkSize),
getPrettySize(restChunkSize),
'',
])
}
Expand Down Expand Up @@ -717,7 +732,11 @@ export async function printTreeView(
)

messages.push(['', '', ''])
messages.push(['ƒ Middleware', getPrettySize(sum(middlewareSizes)), ''])
messages.push([
'ƒ Middleware',
getPrettySize(sum(middlewareSizes), { strong: true }),
'',
])
}

print(
Expand Down
3 changes: 2 additions & 1 deletion test/ppr-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@
"test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts",
"test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts",
"test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts",
"test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts"
"test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts",
"test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { nextTestSetup } from 'e2e-utils'
import path from 'path'

describe('build-output-tree-view', () => {
describe('with mixed static and dynamic pages and app router routes', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixtures/mixed'),
skipStart: true,
env: {
__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1',
},
})

beforeAll(() => next.build())

it('should show info about prerendered and dynamic routes in a tree view', async () => {
// TODO: Show cache info (revalidate/expire) for app router, and use the
// same for pages router instead of the ISR addendum.

// TODO: Fix double-listing of the /ppr/[slug] fallback.

expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
"Route (app) Size First Load JS
┌ ○ /_not-found N/A kB N/A kB
├ ƒ /api N/A kB N/A kB
├ ○ /api/force-static N/A kB N/A kB
├ ○ /app-static N/A kB N/A kB
├ ○ /cache-life N/A kB N/A kB
├ ƒ /dynamic N/A kB N/A kB
├ ◐ /ppr/[slug] N/A kB N/A kB
├ ├ /ppr/[slug]
├ ├ /ppr/[slug]
├ ├ /ppr/days
├ └ /ppr/weeks
└ ○ /revalidate N/A kB N/A kB
+ First Load JS shared by all N/A kB
Route (pages) Size First Load JS
┌ ƒ /api/hello N/A kB N/A kB
├ ● /gsp-revalidate (ISR: 300 Seconds) N/A kB N/A kB
├ ƒ /gssp N/A kB N/A kB
└ ○ /static N/A kB N/A kB
+ First Load JS shared by all N/A kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
(ISR) incremental static regeneration (uses revalidate in generateStaticParams)
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
ƒ (Dynamic) server-rendered on demand"
`)
})
})

describe('with only a few static routes', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixtures/minimal-static'),
skipStart: true,
env: {
__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1',
},
})

beforeAll(() => next.build())

it('should show info about prerendered routes in a compact tree view', async () => {
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
"Route (app) Size First Load JS
┌ ○ / N/A kB N/A kB
└ ○ /_not-found N/A kB N/A kB
+ First Load JS shared by all N/A kB
Route (pages) Size First Load JS
─ ○ /static N/A kB N/A kB
+ First Load JS shared by all N/A kB
○ (Static) prerendered as static content"
`)
})
})
})

function getTreeView(cliOutput: string): string {
let foundBuildTracesLine = false
const lines: string[] = []

for (const line of cliOutput.split('\n')) {
if (foundBuildTracesLine) {
lines.push(line)
}

foundBuildTracesLine ||= line.includes('Collecting build traces')
}

return lines.join('\n').trim()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const dynamic = 'force-static'

export async function GET() {
return Response.json({ message: 'hello world' })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ message: 'hello world' })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use cache'

import { unstable_cacheLife } from 'next/cache'

export default async function Page() {
unstable_cacheLife('weeks')

return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { headers } from 'next/headers'

export default async function Page() {
await headers()

return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { unstable_cacheLife } from 'next/cache'

type CacheLife = Parameters<typeof unstable_cacheLife>[0]

async function getCachedValue(cacheLife: CacheLife) {
'use cache'

unstable_cacheLife(cacheLife)

return Math.random()
}

export default async function Page({
params,
}: {
params: Promise<{ slug: CacheLife }>
}) {
const { slug } = await params

return <p>hello world {await getCachedValue(slug)}</p>
}

export function generateStaticParams() {
return [{ slug: 'days' }, { slug: 'weeks' }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const revalidate = 900

export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
useCache: true,
ppr: true,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ message: 'hello world' })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export async function getStaticProps() {
return { props: {}, revalidate: 300 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export async function getServerSideProps() {
return { props: {} }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}

0 comments on commit 3d065cf

Please sign in to comment.