Skip to content

Commit 3d065cf

Browse files
authored
Add test to assert on current build output tree view (#76342)
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.
1 parent b84bd52 commit 3d065cf

File tree

20 files changed

+246
-13
lines changed

20 files changed

+246
-13
lines changed

packages/next/src/build/utils.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,22 @@ export async function printTreeView(
402402
gzipSize?: boolean
403403
}
404404
) {
405-
const getPrettySize = (_size: number): string => {
406-
const size = prettyBytes(_size)
407-
return white(bold(size))
405+
const getPrettySize = (
406+
_size: number,
407+
{ strong }: { strong?: boolean } = {}
408+
): string => {
409+
const size = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
410+
? 'N/A kB'
411+
: prettyBytes(_size)
412+
413+
return strong ? white(bold(size)) : size
408414
}
409415

410-
const MIN_DURATION = 300
416+
// Can be overridden for test purposes to omit the build duration output.
417+
const MIN_DURATION = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
418+
? Infinity // Don't ever log build durations.
419+
: 300
420+
411421
const getPrettyDuration = (_duration: number): string => {
412422
const duration = `${_duration} ms`
413423
// green for 300-1000ms
@@ -526,14 +536,14 @@ export async function printTreeView(
526536
? ampFirst
527537
? cyan('AMP')
528538
: pageInfo.size >= 0
529-
? prettyBytes(pageInfo.size)
539+
? getPrettySize(pageInfo.size)
530540
: ''
531541
: '',
532542
pageInfo
533543
? ampFirst
534544
? cyan('AMP')
535545
: pageInfo.size >= 0
536-
? getPrettySize(pageInfo.totalSize)
546+
? getPrettySize(pageInfo.totalSize, { strong: true })
537547
: ''
538548
: '',
539549
])
@@ -553,7 +563,7 @@ export async function printTreeView(
553563
const size = stats.sizes.get(file)
554564
messages.push([
555565
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
556-
typeof size === 'number' ? prettyBytes(size) : '',
566+
typeof size === 'number' ? getPrettySize(size) : '',
557567
'',
558568
])
559569
})
@@ -628,11 +638,16 @@ export async function printTreeView(
628638
})
629639

630640
const sharedFilesSize = stats.router[routerType]?.common.size.total
631-
const sharedFiles = stats.router[routerType]?.common.files ?? []
641+
642+
const sharedFiles = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
643+
? []
644+
: stats.router[routerType]?.common.files ?? []
632645

633646
messages.push([
634647
'+ First Load JS shared by all',
635-
typeof sharedFilesSize === 'number' ? getPrettySize(sharedFilesSize) : '',
648+
typeof sharedFilesSize === 'number'
649+
? getPrettySize(sharedFilesSize, { strong: true })
650+
: '',
636651
'',
637652
])
638653
const sharedCssFiles: string[] = []
@@ -667,13 +682,13 @@ export async function printTreeView(
667682
return
668683
}
669684

670-
messages.push([` ${innerSymbol} ${cleanName}`, prettyBytes(size), ''])
685+
messages.push([` ${innerSymbol} ${cleanName}`, getPrettySize(size), ''])
671686
})
672687

673688
if (restChunkCount > 0) {
674689
messages.push([
675690
` └ other shared chunks (total)`,
676-
prettyBytes(restChunkSize),
691+
getPrettySize(restChunkSize),
677692
'',
678693
])
679694
}
@@ -717,7 +732,11 @@ export async function printTreeView(
717732
)
718733

719734
messages.push(['', '', ''])
720-
messages.push(['ƒ Middleware', getPrettySize(sum(middlewareSizes)), ''])
735+
messages.push([
736+
'ƒ Middleware',
737+
getPrettySize(sum(middlewareSizes), { strong: true }),
738+
'',
739+
])
721740
}
722741

723742
print(

test/ppr-tests-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
"test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts",
101101
"test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts",
102102
"test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts",
103-
"test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts"
103+
"test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts",
104+
"test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts"
104105
]
105106
}
106107
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import path from 'path'
3+
4+
describe('build-output-tree-view', () => {
5+
describe('with mixed static and dynamic pages and app router routes', () => {
6+
const { next } = nextTestSetup({
7+
files: path.join(__dirname, 'fixtures/mixed'),
8+
skipStart: true,
9+
env: {
10+
__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1',
11+
},
12+
})
13+
14+
beforeAll(() => next.build())
15+
16+
it('should show info about prerendered and dynamic routes in a tree view', async () => {
17+
// TODO: Show cache info (revalidate/expire) for app router, and use the
18+
// same for pages router instead of the ISR addendum.
19+
20+
// TODO: Fix double-listing of the /ppr/[slug] fallback.
21+
22+
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
23+
"Route (app) Size First Load JS
24+
┌ ○ /_not-found N/A kB N/A kB
25+
├ ƒ /api N/A kB N/A kB
26+
├ ○ /api/force-static N/A kB N/A kB
27+
├ ○ /app-static N/A kB N/A kB
28+
├ ○ /cache-life N/A kB N/A kB
29+
├ ƒ /dynamic N/A kB N/A kB
30+
├ ◐ /ppr/[slug] N/A kB N/A kB
31+
├ ├ /ppr/[slug]
32+
├ ├ /ppr/[slug]
33+
├ ├ /ppr/days
34+
├ └ /ppr/weeks
35+
└ ○ /revalidate N/A kB N/A kB
36+
+ First Load JS shared by all N/A kB
37+
38+
Route (pages) Size First Load JS
39+
┌ ƒ /api/hello N/A kB N/A kB
40+
├ ● /gsp-revalidate (ISR: 300 Seconds) N/A kB N/A kB
41+
├ ƒ /gssp N/A kB N/A kB
42+
└ ○ /static N/A kB N/A kB
43+
+ First Load JS shared by all N/A kB
44+
45+
○ (Static) prerendered as static content
46+
● (SSG) prerendered as static HTML (uses generateStaticParams)
47+
(ISR) incremental static regeneration (uses revalidate in generateStaticParams)
48+
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
49+
ƒ (Dynamic) server-rendered on demand"
50+
`)
51+
})
52+
})
53+
54+
describe('with only a few static routes', () => {
55+
const { next } = nextTestSetup({
56+
files: path.join(__dirname, 'fixtures/minimal-static'),
57+
skipStart: true,
58+
env: {
59+
__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1',
60+
},
61+
})
62+
63+
beforeAll(() => next.build())
64+
65+
it('should show info about prerendered routes in a compact tree view', async () => {
66+
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
67+
"Route (app) Size First Load JS
68+
┌ ○ / N/A kB N/A kB
69+
└ ○ /_not-found N/A kB N/A kB
70+
+ First Load JS shared by all N/A kB
71+
72+
Route (pages) Size First Load JS
73+
─ ○ /static N/A kB N/A kB
74+
+ First Load JS shared by all N/A kB
75+
76+
○ (Static) prerendered as static content"
77+
`)
78+
})
79+
})
80+
})
81+
82+
function getTreeView(cliOutput: string): string {
83+
let foundBuildTracesLine = false
84+
const lines: string[] = []
85+
86+
for (const line of cliOutput.split('\n')) {
87+
if (foundBuildTracesLine) {
88+
lines.push(line)
89+
}
90+
91+
foundBuildTracesLine ||= line.includes('Collecting build traces')
92+
}
93+
94+
return lines.join('\n').trim()
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const dynamic = 'force-static'
2+
3+
export async function GET() {
4+
return Response.json({ message: 'hello world' })
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function GET() {
2+
return Response.json({ message: 'hello world' })
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use cache'
2+
3+
import { unstable_cacheLife } from 'next/cache'
4+
5+
export default async function Page() {
6+
unstable_cacheLife('weeks')
7+
8+
return <p>hello world</p>
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { headers } from 'next/headers'
2+
3+
export default async function Page() {
4+
await headers()
5+
6+
return <p>hello world</p>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { unstable_cacheLife } from 'next/cache'
2+
3+
type CacheLife = Parameters<typeof unstable_cacheLife>[0]
4+
5+
async function getCachedValue(cacheLife: CacheLife) {
6+
'use cache'
7+
8+
unstable_cacheLife(cacheLife)
9+
10+
return Math.random()
11+
}
12+
13+
export default async function Page({
14+
params,
15+
}: {
16+
params: Promise<{ slug: CacheLife }>
17+
}) {
18+
const { slug } = await params
19+
20+
return <p>hello world {await getCachedValue(slug)}</p>
21+
}
22+
23+
export function generateStaticParams() {
24+
return [{ slug: 'days' }, { slug: 'weeks' }]
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const revalidate = 900
2+
3+
export default function Page() {
4+
return <p>hello world</p>
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
experimental: {
6+
useCache: true,
7+
ppr: true,
8+
},
9+
}
10+
11+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next'
2+
3+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
4+
res.status(200).json({ message: 'hello world' })
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
4+
5+
export async function getStaticProps() {
6+
return { props: {}, revalidate: 300 }
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
4+
5+
export async function getServerSideProps() {
6+
return { props: {} }
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}

0 commit comments

Comments
 (0)