App Build Stats (#38884)

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)


Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
This commit is contained in:
Wyatt Johnson 2022-08-10 13:31:01 -06:00 committed by GitHub
parent b15a976e11
commit f5cab2f515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 827 additions and 494 deletions

View file

@ -19,7 +19,7 @@ import {
import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
EDGE_RUNTIME_WEBPACK,
} from '../shared/lib/constants'
@ -506,14 +506,14 @@ export function finalizeEntrypoint({
// Client special cases
name !== 'polyfills' &&
name !== CLIENT_STATIC_FILES_RUNTIME_MAIN &&
name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT &&
name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_APP &&
name !== CLIENT_STATIC_FILES_RUNTIME_AMP &&
name !== CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH
) {
// TODO-APP: this is a temporary fix. @shuding is going to change the handling of server components
if (appDir && entry.import.includes('flight')) {
return {
dependOn: CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT,
dependOn: CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
...entry,
}
}

View file

@ -55,6 +55,7 @@ import {
MIDDLEWARE_MANIFEST,
APP_PATHS_MANIFEST,
APP_PATH_ROUTES_MANIFEST,
APP_BUILD_MANIFEST,
} from '../shared/lib/constants'
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
import { __ApiPreviewProps } from '../server/api-utils'
@ -116,6 +117,7 @@ import { flatReaddir } from '../lib/flat-readdir'
import { RemotePattern } from '../shared/lib/image-config'
import { eventSwcPlugins } from '../telemetry/events/swc-plugins'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
export type SsgRoute = {
initialRevalidateSeconds: number | false
@ -333,7 +335,7 @@ export default async function build(
const isLikeServerless = isTargetLikeServerless(target)
const pagePaths = await nextBuildSpan
const pagesPaths = await nextBuildSpan
.traceChild('collect-pages')
.traceAsyncFn(() =>
recursiveReadDir(
@ -383,14 +385,14 @@ export default async function build(
isDev: false,
pageExtensions: config.pageExtensions,
pagesType: 'pages',
pagePaths: pagePaths,
pagePaths: pagesPaths,
})
)
let mappedAppPaths: { [page: string]: string } | undefined
let mappedAppPages: { [page: string]: string } | undefined
if (appPaths && appDir) {
mappedAppPaths = nextBuildSpan
mappedAppPages = nextBuildSpan
.traceChild('create-app-mapping')
.traceFn(() =>
createPagesMapping({
@ -429,12 +431,18 @@ export default async function build(
rootDir: dir,
rootPaths: mappedRootPaths,
appDir,
appPaths: mappedAppPaths,
appPaths: mappedAppPages,
pageExtensions: config.pageExtensions,
})
)
const pageKeys = Object.keys(mappedPages)
const pageKeys = {
pages: Object.keys(mappedPages),
app: mappedAppPages
? Object.keys(mappedAppPages).map((key) => normalizeAppPath(key))
: undefined,
}
const conflictingPublicFiles: string[] = []
const hasPages404 = mappedPages['/404']?.startsWith(PAGES_DIR_ALIAS)
const hasCustomErrorPage =
@ -477,7 +485,7 @@ export default async function build(
}
})
const nestedReservedPages = pageKeys.filter((page) => {
const nestedReservedPages = pageKeys.pages.filter((page) => {
return (
page.match(/\/(_app|_document|_error)$/) && path.dirname(page) !== '/'
)
@ -578,10 +586,8 @@ export default async function build(
}
} = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => {
const sortedRoutes = getSortedRoutes([
...pageKeys,
...Object.keys(mappedAppPaths || {}).map((key) =>
normalizeAppPath(key)
),
...pageKeys.pages,
...(pageKeys.app ?? []),
])
const dynamicRoutes: Array<ReturnType<typeof pageToRoute>> = []
const staticRoutes: typeof dynamicRoutes = []
@ -911,7 +917,7 @@ export default async function build(
throw err
} else {
telemetry.record(
eventBuildCompleted(pagePaths, {
eventBuildCompleted(pagesPaths, {
durationInSeconds: webpackBuildEnd[0],
})
)
@ -930,6 +936,7 @@ export default async function build(
})
const buildManifestPath = path.join(distDir, BUILD_MANIFEST)
const appBuildManifestPath = path.join(distDir, APP_BUILD_MANIFEST)
const ssgPages = new Set<string>()
const ssgStaticFallbackPages = new Set<string>()
@ -949,6 +956,11 @@ export default async function build(
const buildManifest = JSON.parse(
await promises.readFile(buildManifestPath, 'utf8')
) as BuildManifest
const appBuildManifest = appDir
? (JSON.parse(
await promises.readFile(appBuildManifestPath, 'utf8')
) as AppBuildManifest)
: undefined
const timeout = config.staticPageGenerationTimeout || 0
const sharedPool = config.experimental.sharedPool || false
@ -1079,188 +1091,222 @@ export default async function build(
let hasSsrAmpPages = false
const computedManifestData = await computeFromManifest(
buildManifest,
{ build: buildManifest, app: appBuildManifest },
distDir,
config.experimental.gzipSize
)
await Promise.all(
pageKeys.map((page) => {
const checkPageSpan = staticCheckSpan.traceChild('check-page', {
page,
})
return checkPageSpan.traceAsyncFn(async () => {
const actualPage = normalizePagePath(page)
const [selfSize, allSize] = await getJsPageSizeInKb(
actualPage,
distDir,
buildManifest,
config.experimental.gzipSize,
computedManifestData
)
let isSsg = false
let isStatic = false
let isServerComponent = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
const pagePath = pagePaths.find(
(p) =>
p.startsWith(actualPage + '.') ||
p.startsWith(actualPage + '/index.')
)
const pageRuntime = pagePath
? (
await getPageStaticInfo({
pageFilePath: join(pagesDir, pagePath),
nextConfig: config,
})
).runtime
: undefined
if (hasServerComponents && pagePath) {
if (isServerComponentPage(config, pagePath)) {
isServerComponent = true
Object.entries(pageKeys)
.reduce<Array<{ pageType: keyof typeof pageKeys; page: string }>>(
(acc, [key, files]) => {
if (!files) {
return acc
}
}
if (
!isReservedPage(page) &&
// We currently don't support static optimization in the Edge runtime.
pageRuntime !== SERVER_RUNTIME.edge
) {
try {
let isPageStaticSpan =
checkPageSpan.traceChild('is-page-static')
let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
return staticWorkers.isPageStatic(
page,
distDir,
isLikeServerless,
configFileName,
runtimeEnvConfig,
config.httpAgentOptions,
config.i18n?.locales,
config.i18n?.defaultLocale,
isPageStaticSpan.id
)
})
const pageType = key as keyof typeof pageKeys
if (config.outputFileTracing) {
pageTraceIncludes.set(
page,
workerResult.traceIncludes || []
)
pageTraceExcludes.set(
page,
workerResult.traceExcludes || []
)
}
for (const page of files) {
acc.push({ pageType, page })
}
if (
workerResult.isStatic === false &&
(workerResult.isHybridAmp || workerResult.isAmpOnly)
) {
hasSsrAmpPages = true
}
return acc
},
[]
)
.map(({ pageType, page }) => {
const checkPageSpan = staticCheckSpan.traceChild('check-page', {
page,
})
return checkPageSpan.traceAsyncFn(async () => {
const actualPage = normalizePagePath(page)
const [selfSize, allSize] = await getJsPageSizeInKb(
pageType,
actualPage,
distDir,
buildManifest,
appBuildManifest,
config.experimental.gzipSize,
computedManifestData
)
if (workerResult.isHybridAmp) {
isHybridAmp = true
hybridAmpPages.add(page)
}
let isSsg = false
let isStatic = false
let isServerComponent = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
if (workerResult.isNextImageImported) {
isNextImageImported = true
}
if (workerResult.hasStaticProps) {
ssgPages.add(page)
isSsg = true
if (
workerResult.prerenderRoutes &&
workerResult.encodedPrerenderRoutes
) {
additionalSsgPaths.set(page, workerResult.prerenderRoutes)
additionalSsgPathsEncoded.set(
page,
workerResult.encodedPrerenderRoutes
const pagePath =
pageType === 'pages'
? pagesPaths.find(
(p) =>
p.startsWith(actualPage + '.') ||
p.startsWith(actualPage + '/index.')
)
ssgPageRoutes = workerResult.prerenderRoutes
}
: appPaths?.find((p) => p.startsWith(actualPage + '/page.'))
if (workerResult.prerenderFallback === 'blocking') {
ssgBlockingFallbackPages.add(page)
} else if (workerResult.prerenderFallback === true) {
ssgStaticFallbackPages.add(page)
}
} else if (workerResult.hasServerProps) {
serverPropsPages.add(page)
} else if (
workerResult.isStatic &&
!isServerComponent &&
(await customAppGetInitialPropsPromise) === false
) {
staticPages.add(page)
isStatic = true
} else if (isServerComponent) {
// This is a static server component page that doesn't have
// gSP or gSSP. We still treat it as a SSG page.
ssgPages.add(page)
isSsg = true
const pageRuntime =
pageType === 'pages' && pagePath
? (
await getPageStaticInfo({
pageFilePath: join(pagesDir, pagePath),
nextConfig: config,
})
).runtime
: undefined
if (hasServerComponents && pagePath) {
if (isServerComponentPage(config, pagePath)) {
isServerComponent = true
}
}
if (
// Only calculate page static information if the page is not an
// app page.
pageType !== 'app' &&
!isReservedPage(page) &&
// We currently don't support static optimization in the Edge runtime.
pageRuntime !== SERVER_RUNTIME.edge
) {
try {
let isPageStaticSpan =
checkPageSpan.traceChild('is-page-static')
let workerResult = await isPageStaticSpan.traceAsyncFn(
() => {
return staticWorkers.isPageStatic(
page,
distDir,
isLikeServerless,
configFileName,
runtimeEnvConfig,
config.httpAgentOptions,
config.i18n?.locales,
config.i18n?.defaultLocale,
isPageStaticSpan.id
)
}
)
if (config.outputFileTracing) {
pageTraceIncludes.set(
page,
workerResult.traceIncludes || []
)
pageTraceExcludes.set(
page,
workerResult.traceExcludes || []
)
}
if (hasPages404 && page === '/404') {
if (
workerResult.isStatic === false &&
(workerResult.isHybridAmp || workerResult.isAmpOnly)
) {
hasSsrAmpPages = true
}
if (workerResult.isHybridAmp) {
isHybridAmp = true
hybridAmpPages.add(page)
}
if (workerResult.isNextImageImported) {
isNextImageImported = true
}
if (workerResult.hasStaticProps) {
ssgPages.add(page)
isSsg = true
if (
workerResult.prerenderRoutes &&
workerResult.encodedPrerenderRoutes
) {
additionalSsgPaths.set(
page,
workerResult.prerenderRoutes
)
additionalSsgPathsEncoded.set(
page,
workerResult.encodedPrerenderRoutes
)
ssgPageRoutes = workerResult.prerenderRoutes
}
if (workerResult.prerenderFallback === 'blocking') {
ssgBlockingFallbackPages.add(page)
} else if (workerResult.prerenderFallback === true) {
ssgStaticFallbackPages.add(page)
}
} else if (workerResult.hasServerProps) {
serverPropsPages.add(page)
} else if (
workerResult.isStatic &&
!isServerComponent &&
(await customAppGetInitialPropsPromise) === false
) {
staticPages.add(page)
isStatic = true
} else if (isServerComponent) {
// This is a static server component page that doesn't have
// gSP or gSSP. We still treat it as a SSG page.
ssgPages.add(page)
isSsg = true
}
if (hasPages404 && page === '/404') {
if (
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
throw new Error(
`\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
)
}
// we need to ensure the 404 lambda is present since we use
// it when _app has getInitialProps
if (
(await customAppGetInitialPropsPromise) &&
!workerResult.hasStaticProps
) {
staticPages.delete(page)
}
}
if (
STATIC_STATUS_PAGES.includes(page) &&
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
throw new Error(
`\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
`\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
)
}
// we need to ensure the 404 lambda is present since we use
// it when _app has getInitialProps
} catch (err) {
if (
(await customAppGetInitialPropsPromise) &&
!workerResult.hasStaticProps
) {
staticPages.delete(page)
}
}
if (
STATIC_STATUS_PAGES.includes(page) &&
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
throw new Error(
`\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
!isError(err) ||
err.message !== 'INVALID_DEFAULT_EXPORT'
)
throw err
invalidPages.add(page)
}
} catch (err) {
if (!isError(err) || err.message !== 'INVALID_DEFAULT_EXPORT')
throw err
invalidPages.add(page)
}
}
pageInfos.set(page, {
size: selfSize,
totalSize: allSize,
static: isStatic,
isSsg,
isHybridAmp,
ssgPageRoutes,
initialRevalidateSeconds: false,
runtime: pageRuntime,
pageDuration: undefined,
ssgPageDurations: undefined,
pageInfos.set(page, {
size: selfSize,
totalSize: allSize,
static: isStatic,
isSsg,
isHybridAmp,
ssgPageRoutes,
initialRevalidateSeconds: false,
runtime: pageRuntime,
pageDuration: undefined,
ssgPageDurations: undefined,
})
})
})
})
)
const errorPageResult = await errorPageStaticResult
@ -1330,7 +1376,7 @@ export default async function build(
})
}
for (let page of pageKeys) {
for (let page of pageKeys.pages) {
await includeExcludeSpan
.traceChild('include-exclude', { page })
.traceAsyncFn(async () => {
@ -1671,7 +1717,7 @@ export default async function build(
await copyTracedFiles(
dir,
distDir,
pageKeys,
pageKeys.pages,
outputFileTracingRoot,
requiredServerFiles.config,
middlewareManifest
@ -1711,7 +1757,7 @@ export default async function build(
detectConflictingPaths(
[
...combinedPages,
...pageKeys.filter((page) => !combinedPages.includes(page)),
...pageKeys.pages.filter((page) => !combinedPages.includes(page)),
],
ssgPages,
additionalSsgPaths
@ -2133,13 +2179,13 @@ export default async function build(
const analysisEnd = process.hrtime(analysisBegin)
telemetry.record(
eventBuildOptimize(pagePaths, {
eventBuildOptimize(pagesPaths, {
durationInSeconds: analysisEnd[0],
staticPageCount: staticPages.size,
staticPropsPageCount: ssgPages.size,
serverPropsPageCount: serverPropsPages.size,
ssrPageCount:
pagePaths.length -
pagesPaths.length -
(staticPages.size + ssgPages.size + serverPropsPages.size),
hasStatic404: useStatic404,
hasReportWebVitals:
@ -2300,21 +2346,17 @@ export default async function build(
})
await nextBuildSpan.traceChild('print-tree-view').traceAsyncFn(() =>
printTreeView(
Object.keys(mappedPages),
allPageInfos,
isLikeServerless,
{
distPath: distDir,
buildId: buildId,
pagesDir,
useStatic404,
pageExtensions: config.pageExtensions,
buildManifest,
middlewareManifest,
gzipSize: config.experimental.gzipSize,
}
)
printTreeView(pageKeys, allPageInfos, isLikeServerless, {
distPath: distDir,
buildId: buildId,
pagesDir,
useStatic404,
pageExtensions: config.pageExtensions,
appBuildManifest,
buildManifest,
middlewareManifest,
gzipSize: config.experimental.gzipSize,
})
)
if (debugOutput) {

View file

@ -42,6 +42,9 @@ import { Sema } from 'next/dist/compiled/async-sema'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
export type ROUTER_TYPE = 'pages' | 'app'
const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/
const fileGzipStats: { [k: string]: Promise<number> | undefined } = {}
@ -76,7 +79,10 @@ export interface PageInfo {
}
export async function printTreeView(
list: readonly string[],
lists: {
pages: ReadonlyArray<string>
app?: ReadonlyArray<string>
},
pageInfos: Map<string, PageInfo>,
serverless: boolean,
{
@ -85,6 +91,7 @@ export async function printTreeView(
pagesDir,
pageExtensions,
buildManifest,
appBuildManifest,
middlewareManifest,
useStatic404,
gzipSize = true,
@ -94,6 +101,7 @@ export async function printTreeView(
pagesDir: string
pageExtensions: string[]
buildManifest: BuildManifest
appBuildManifest?: AppBuildManifest
middlewareManifest: MiddlewareManifest
useStatic404: boolean
gzipSize?: boolean
@ -129,227 +137,266 @@ export async function printTreeView(
// Remove file hash
.replace(/(?:^|[.-])([0-9a-z]{6})[0-9a-z]{14}(?=\.)/, '.$1')
const messages: [string, string, string][] = [
['Page', 'Size', 'First Load JS'].map((entry) =>
chalk.underline(entry)
) as [string, string, string],
]
// Check if we have a custom app.
const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions)
pageInfos.set('/404', {
...(pageInfos.get('/404') || pageInfos.get('/_error')),
static: useStatic404,
} as any)
if (!list.includes('/404')) {
list = [...list, '/404']
}
const filterAndSortList = (list: ReadonlyArray<string>) =>
list
.slice()
.filter(
(e) =>
!(
e === '/_document' ||
e === '/_error' ||
(!hasCustomApp && e === '/_app')
)
)
.sort((a, b) => a.localeCompare(b))
const sizeData = await computeFromManifest(
buildManifest,
// Collect all the symbols we use so we can print the icons out.
const usedSymbols = new Set()
const messages: [string, string, string][] = []
const stats = await computeFromManifest(
{ build: buildManifest, app: appBuildManifest },
distPath,
gzipSize,
pageInfos
)
const usedSymbols = new Set()
const pageList = list
.slice()
.filter(
(e) =>
!(
e === '/_document' ||
e === '/_error' ||
(!hasCustomApp && e === '/_app')
)
const printFileTree = async ({
list,
routerType,
}: {
list: ReadonlyArray<string>
routerType: ROUTER_TYPE
}) => {
messages.push(
[
routerType === 'app' ? 'Route (app)' : 'Route (pages)',
'Size',
'First Load JS',
].map((entry) => chalk.underline(entry)) as [string, string, string]
)
.sort((a, b) => a.localeCompare(b))
pageList.forEach((item, i, arr) => {
const border =
i === 0
? arr.length === 1
? '─'
: '┌'
: i === arr.length - 1
? '└'
: '├'
filterAndSortList(list).forEach((item, i, arr) => {
const border =
i === 0
? arr.length === 1
? '─'
: '┌'
: i === arr.length - 1
? '└'
: '├'
const pageInfo = pageInfos.get(item)
const ampFirst = buildManifest.ampFirstPages.includes(item)
const totalDuration =
(pageInfo?.pageDuration || 0) +
(pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0)
const pageInfo = pageInfos.get(item)
const ampFirst = buildManifest.ampFirstPages.includes(item)
const totalDuration =
(pageInfo?.pageDuration || 0) +
(pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0)
const symbol =
item === '/_app' || item === '/_app.server'
? ' '
: pageInfo?.static
? '○'
: pageInfo?.isSsg
? '●'
: pageInfo?.runtime === SERVER_RUNTIME.edge
? 'ℇ'
: 'λ'
const symbol =
routerType === 'app' || item === '/_app' || item === '/_app.server'
? ' '
: pageInfo?.static
? '○'
: pageInfo?.isSsg
? '●'
: pageInfo?.runtime === SERVER_RUNTIME.edge
? 'ℇ'
: 'λ'
usedSymbols.add(symbol)
usedSymbols.add(symbol)
if (pageInfo?.initialRevalidateSeconds) usedSymbols.add('ISR')
if (pageInfo?.initialRevalidateSeconds) usedSymbols.add('ISR')
messages.push([
`${border} ${symbol} ${
pageInfo?.initialRevalidateSeconds
? `${item} (ISR: ${pageInfo?.initialRevalidateSeconds} Seconds)`
: item
}${
totalDuration > MIN_DURATION
? ` (${getPrettyDuration(totalDuration)})`
: ''
}`,
pageInfo
? ampFirst
? chalk.cyan('AMP')
: pageInfo.size >= 0
? prettyBytes(pageInfo.size)
: ''
: '',
pageInfo
? ampFirst
? chalk.cyan('AMP')
: pageInfo.size >= 0
? getPrettySize(pageInfo.totalSize)
: ''
: '',
])
messages.push([
`${border} ${routerType === 'pages' ? `${symbol} ` : ''}${
pageInfo?.initialRevalidateSeconds
? `${item} (ISR: ${pageInfo?.initialRevalidateSeconds} Seconds)`
: item
}${
totalDuration > MIN_DURATION
? ` (${getPrettyDuration(totalDuration)})`
: ''
}`,
pageInfo
? ampFirst
? chalk.cyan('AMP')
: pageInfo.size >= 0
? prettyBytes(pageInfo.size)
: ''
: '',
pageInfo
? ampFirst
? chalk.cyan('AMP')
: pageInfo.size >= 0
? getPrettySize(pageInfo.totalSize)
: ''
: '',
])
const uniqueCssFiles =
buildManifest.pages[item]?.filter(
(file) => file.endsWith('.css') && sizeData.uniqueFiles.includes(file)
) || []
const uniqueCssFiles =
buildManifest.pages[item]?.filter(
(file) =>
file.endsWith('.css') &&
stats.router[routerType]?.unique.files.includes(file)
) || []
if (uniqueCssFiles.length > 0) {
const contSymbol = i === arr.length - 1 ? ' ' : '├'
if (uniqueCssFiles.length > 0) {
const contSymbol = i === arr.length - 1 ? ' ' : '├'
uniqueCssFiles.forEach((file, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
messages.push([
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
prettyBytes(sizeData.sizeUniqueFiles[file]),
'',
])
})
}
if (pageInfo?.ssgPageRoutes?.length) {
const totalRoutes = pageInfo.ssgPageRoutes.length
const contSymbol = i === arr.length - 1 ? ' ' : '├'
let routes: { route: string; duration: number; avgDuration?: number }[]
if (
pageInfo.ssgPageDurations &&
pageInfo.ssgPageDurations.some((d) => d > MIN_DURATION)
) {
const previewPages = totalRoutes === 8 ? 8 : Math.min(totalRoutes, 7)
const routesWithDuration = pageInfo.ssgPageRoutes
.map((route, idx) => ({
route,
duration: pageInfo.ssgPageDurations![idx] || 0,
}))
.sort(({ duration: a }, { duration: b }) =>
// Sort by duration
// keep too small durations in original order at the end
a <= MIN_DURATION && b <= MIN_DURATION ? 0 : b - a
)
routes = routesWithDuration.slice(0, previewPages)
const remainingRoutes = routesWithDuration.slice(previewPages)
if (remainingRoutes.length) {
const remaining = remainingRoutes.length
const avgDuration = Math.round(
remainingRoutes.reduce(
(total, { duration }) => total + duration,
0
) / remainingRoutes.length
)
routes.push({
route: `[+${remaining} more paths]`,
duration: 0,
avgDuration,
})
}
} else {
const previewPages = totalRoutes === 4 ? 4 : Math.min(totalRoutes, 3)
routes = pageInfo.ssgPageRoutes
.slice(0, previewPages)
.map((route) => ({ route, duration: 0 }))
if (totalRoutes > previewPages) {
const remaining = totalRoutes - previewPages
routes.push({ route: `[+${remaining} more paths]`, duration: 0 })
}
uniqueCssFiles.forEach((file, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
const size = stats.sizes.get(file)
messages.push([
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
typeof size === 'number' ? prettyBytes(size) : '',
'',
])
})
}
routes.forEach(({ route, duration, avgDuration }, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
messages.push([
`${contSymbol} ${innerSymbol} ${route}${
duration > MIN_DURATION ? ` (${getPrettyDuration(duration)})` : ''
}${
avgDuration && avgDuration > MIN_DURATION
? ` (avg ${getPrettyDuration(avgDuration)})`
: ''
}`,
'',
'',
])
})
}
})
if (pageInfo?.ssgPageRoutes?.length) {
const totalRoutes = pageInfo.ssgPageRoutes.length
const contSymbol = i === arr.length - 1 ? ' ' : '├'
const sharedFilesSize = sizeData.sizeCommonFiles
const sharedFiles = sizeData.sizeCommonFile
messages.push([
'+ First Load JS shared by all',
getPrettySize(sharedFilesSize),
'',
])
const sharedFileKeys = Object.keys(sharedFiles)
const sharedCssFiles: string[] = []
;[
...sharedFileKeys
.filter((file) => {
if (file.endsWith('.css')) {
sharedCssFiles.push(file)
return false
let routes: { route: string; duration: number; avgDuration?: number }[]
if (
pageInfo.ssgPageDurations &&
pageInfo.ssgPageDurations.some((d) => d > MIN_DURATION)
) {
const previewPages = totalRoutes === 8 ? 8 : Math.min(totalRoutes, 7)
const routesWithDuration = pageInfo.ssgPageRoutes
.map((route, idx) => ({
route,
duration: pageInfo.ssgPageDurations![idx] || 0,
}))
.sort(({ duration: a }, { duration: b }) =>
// Sort by duration
// keep too small durations in original order at the end
a <= MIN_DURATION && b <= MIN_DURATION ? 0 : b - a
)
routes = routesWithDuration.slice(0, previewPages)
const remainingRoutes = routesWithDuration.slice(previewPages)
if (remainingRoutes.length) {
const remaining = remainingRoutes.length
const avgDuration = Math.round(
remainingRoutes.reduce(
(total, { duration }) => total + duration,
0
) / remainingRoutes.length
)
routes.push({
route: `[+${remaining} more paths]`,
duration: 0,
avgDuration,
})
}
} else {
const previewPages = totalRoutes === 4 ? 4 : Math.min(totalRoutes, 3)
routes = pageInfo.ssgPageRoutes
.slice(0, previewPages)
.map((route) => ({ route, duration: 0 }))
if (totalRoutes > previewPages) {
const remaining = totalRoutes - previewPages
routes.push({ route: `[+${remaining} more paths]`, duration: 0 })
}
}
return true
})
.map((e) => e.replace(buildId, '<buildId>'))
.sort(),
...sharedCssFiles.map((e) => e.replace(buildId, '<buildId>')).sort(),
].forEach((fileName, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
const originalName = fileName.replace('<buildId>', buildId)
const cleanName = getCleanName(fileName)
routes.forEach(
({ route, duration, avgDuration }, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
messages.push([
`${contSymbol} ${innerSymbol} ${route}${
duration > MIN_DURATION
? ` (${getPrettyDuration(duration)})`
: ''
}${
avgDuration && avgDuration > MIN_DURATION
? ` (avg ${getPrettyDuration(avgDuration)})`
: ''
}`,
'',
'',
])
}
)
}
})
const sharedFilesSize = stats.router[routerType]?.common.size.total
const sharedFiles = stats.router[routerType]?.common.files ?? []
messages.push([
` ${innerSymbol} ${cleanName}`,
prettyBytes(sharedFiles[originalName]),
'+ First Load JS shared by all',
typeof sharedFilesSize === 'number' ? getPrettySize(sharedFilesSize) : '',
'',
])
const sharedCssFiles: string[] = []
;[
...sharedFiles
.filter((file) => {
if (file.endsWith('.css')) {
sharedCssFiles.push(file)
return false
}
return true
})
.map((e) => e.replace(buildId, '<buildId>'))
.sort(),
...sharedCssFiles.map((e) => e.replace(buildId, '<buildId>')).sort(),
].forEach((fileName, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'
const originalName = fileName.replace('<buildId>', buildId)
const cleanName = getCleanName(fileName)
const size = stats.sizes.get(originalName)
messages.push([
` ${innerSymbol} ${cleanName}`,
typeof size === 'number' ? prettyBytes(size) : '',
'',
])
})
}
// If enabled, then print the tree for the app directory.
if (lists.app && stats.router.app) {
await printFileTree({
routerType: 'app',
list: lists.app,
})
messages.push(['', '', ''])
}
pageInfos.set('/404', {
...(pageInfos.get('/404') || pageInfos.get('/_error')),
static: useStatic404,
} as any)
if (!lists.pages.includes('/404')) {
lists.pages = [...lists.pages, '/404']
}
// Print the tree view for the pages directory.
await printFileTree({
routerType: 'pages',
list: lists.pages,
})
const middlewareInfo = middlewareManifest.middleware?.['/']
if (middlewareInfo?.files.length > 0) {
const sizes = await Promise.all(
const middlewareSizes = await Promise.all(
middlewareInfo.files
.map((dep) => `${distPath}/${dep}`)
.map(gzipSize ? fsStatGzip : fsStat)
)
messages.push(['', '', ''])
messages.push(['ƒ Middleware', getPrettySize(sum(sizes)), ''])
messages.push(['ƒ Middleware', getPrettySize(sum(middlewareSizes)), ''])
}
console.log(
@ -479,165 +526,281 @@ export function printCustomRoutes({
}
}
type ComputeManifestShape = {
commonFiles: string[]
uniqueFiles: string[]
sizeUniqueFiles: { [file: string]: number }
sizeCommonFile: { [file: string]: number }
sizeCommonFiles: number
type ComputeFilesGroup = {
files: ReadonlyArray<string>
size: {
total: number
}
}
type ComputeFilesManifest = {
unique: ComputeFilesGroup
common: ComputeFilesGroup
}
type ComputeFilesManifestResult = {
router: {
pages: ComputeFilesManifest
app?: ComputeFilesManifest
}
sizes: Map<string, number>
}
let cachedBuildManifest: BuildManifest | undefined
let cachedAppBuildManifest: AppBuildManifest | undefined
let lastCompute: ComputeManifestShape | undefined
let lastCompute: ComputeFilesManifestResult | undefined
let lastComputePageInfo: boolean | undefined
export async function computeFromManifest(
manifest: BuildManifest,
manifests: {
build: BuildManifest
app?: AppBuildManifest
},
distPath: string,
gzipSize: boolean = true,
pageInfos?: Map<string, PageInfo>
): Promise<ComputeManifestShape> {
): Promise<ComputeFilesManifestResult> {
if (
Object.is(cachedBuildManifest, manifest) &&
lastComputePageInfo === !!pageInfos
Object.is(cachedBuildManifest, manifests.build) &&
lastComputePageInfo === !!pageInfos &&
Object.is(cachedAppBuildManifest, manifests.app)
) {
return lastCompute!
}
let expected = 0
const files = new Map<string, number>()
Object.keys(manifest.pages).forEach((key) => {
// Determine the files that are in pages and app and count them, this will
// tell us if they are unique or common.
const countBuildFiles = (
map: Map<string, number>,
key: string,
manifest: Record<string, ReadonlyArray<string>>
) => {
for (const file of manifest[key]) {
if (key === '/_app') {
map.set(file, Infinity)
} else if (map.has(file)) {
map.set(file, map.get(file)! + 1)
} else {
map.set(file, 1)
}
}
}
const files: {
pages: {
each: Map<string, number>
expected: number
}
app?: {
each: Map<string, number>
expected: number
}
} = {
pages: { each: new Map(), expected: 0 },
}
for (const key in manifests.build.pages) {
if (pageInfos) {
const pageInfo = pageInfos.get(key)
// don't include AMP pages since they don't rely on shared bundles
// AMP First pages are not under the pageInfos key
if (pageInfo?.isHybridAmp) {
return
continue
}
}
++expected
manifest.pages[key].forEach((file) => {
if (key === '/_app') {
files.set(file, Infinity)
} else if (files.has(file)) {
files.set(file, files.get(file)! + 1)
} else {
files.set(file, 1)
}
})
})
const getSize = gzipSize ? fsStatGzip : fsStat
const commonFiles = [...files.entries()]
.filter(([, len]) => len === expected || len === Infinity)
.map(([f]) => f)
const uniqueFiles = [...files.entries()]
.filter(([, len]) => len === 1)
.map(([f]) => f)
let stats: [string, number][]
try {
stats = await Promise.all(
commonFiles.map(
async (f) =>
[f, await getSize(path.join(distPath, f))] as [string, number]
)
)
} catch (_) {
stats = []
files.pages.expected++
countBuildFiles(files.pages.each, key, manifests.build.pages)
}
let uniqueStats: [string, number][]
try {
uniqueStats = await Promise.all(
uniqueFiles.map(
async (f) =>
[f, await getSize(path.join(distPath, f))] as [string, number]
// Collect the build files form the app manifest.
if (manifests.app?.pages) {
files.app = { each: new Map<string, number>(), expected: 0 }
for (const key in manifests.app.pages) {
files.app.expected++
countBuildFiles(files.app.each, key, manifests.app.pages)
}
}
const getSize = gzipSize ? fsStatGzip : fsStat
const stats = new Map<string, number>()
// For all of the files in the pages and app manifests, compute the file size
// at once.
await Promise.all(
[
...new Set<string>([
...files.pages.each.keys(),
...(files.app?.each.keys() ?? []),
]),
].map(async (f) => {
try {
// Add the file size to the stats.
stats.set(f, await getSize(path.join(distPath, f)))
} catch {}
})
)
const groupFiles = async (listing: {
each: Map<string, number>
expected: number
}): Promise<ComputeFilesManifest> => {
const entries = [...listing.each.entries()]
const shapeGroup = (group: [string, number][]): ComputeFilesGroup =>
group.reduce(
(acc, [f]) => {
acc.files.push(f)
const size = stats.get(f)
if (typeof size === 'number') {
acc.size.total += size
}
return acc
},
{
files: [] as string[],
size: {
total: 0,
},
}
)
)
} catch (_) {
uniqueStats = []
return {
unique: shapeGroup(entries.filter(([, len]) => len === 1)),
common: shapeGroup(
entries.filter(
([, len]) => len === listing.expected || len === Infinity
)
),
}
}
lastCompute = {
commonFiles,
uniqueFiles,
sizeUniqueFiles: uniqueStats.reduce(
(obj, n) => Object.assign(obj, { [n[0]]: n[1] }),
{}
),
sizeCommonFile: stats.reduce(
(obj, n) => Object.assign(obj, { [n[0]]: n[1] }),
{}
),
sizeCommonFiles: stats.reduce((size, [f, stat]) => {
if (f.endsWith('.css')) return size
return size + stat
}, 0),
router: {
pages: await groupFiles(files.pages),
app: files.app ? await groupFiles(files.app) : undefined,
},
sizes: stats,
}
cachedBuildManifest = manifest
cachedBuildManifest = manifests.build
cachedAppBuildManifest = manifests.app
lastComputePageInfo = !!pageInfos
return lastCompute!
}
export function difference<T>(main: T[] | Set<T>, sub: T[] | Set<T>): T[] {
export function unique<T>(main: ReadonlyArray<T>, sub: ReadonlyArray<T>): T[] {
return [...new Set([...main, ...sub])]
}
export function difference<T>(
main: ReadonlyArray<T> | ReadonlySet<T>,
sub: ReadonlyArray<T> | ReadonlySet<T>
): T[] {
const a = new Set(main)
const b = new Set(sub)
return [...a].filter((x) => !b.has(x))
}
function intersect<T>(main: T[], sub: T[]): T[] {
/**
* Return an array of the items shared by both arrays.
*/
function intersect<T>(main: ReadonlyArray<T>, sub: ReadonlyArray<T>): T[] {
const a = new Set(main)
const b = new Set(sub)
return [...new Set([...a].filter((x) => b.has(x)))]
}
function sum(a: number[]): number {
function sum(a: ReadonlyArray<number>): number {
return a.reduce((size, stat) => size + stat, 0)
}
function denormalizeAppPagePath(page: string): string {
return page + '/page'
}
export async function getJsPageSizeInKb(
routerType: ROUTER_TYPE,
page: string,
distPath: string,
buildManifest: BuildManifest,
appBuildManifest?: AppBuildManifest,
gzipSize: boolean = true,
computedManifestData?: ComputeManifestShape
cachedStats?: ComputeFilesManifestResult
): Promise<[number, number]> {
const data =
computedManifestData ||
(await computeFromManifest(buildManifest, distPath, gzipSize))
const pageManifest = routerType === 'pages' ? buildManifest : appBuildManifest
if (!pageManifest) {
throw new Error('expected appBuildManifest with an "app" pageType')
}
// If stats was not provided, then compute it again.
const stats =
cachedStats ??
(await computeFromManifest(
{ build: buildManifest, app: appBuildManifest },
distPath,
gzipSize
))
const pageData = stats.router[routerType]
if (!pageData) {
// This error shouldn't happen and represents an error in Next.js.
throw new Error('expected "app" manifest data with an "app" pageType')
}
const pagePath =
routerType === 'pages'
? denormalizePagePath(page)
: denormalizeAppPagePath(page)
const fnFilterJs = (entry: string) => entry.endsWith('.js')
const pageFiles = (
buildManifest.pages[denormalizePagePath(page)] || []
).filter(fnFilterJs)
const appFiles = (buildManifest.pages['/_app'] || []).filter(fnFilterJs)
const pageFiles = (pageManifest.pages[pagePath] ?? []).filter(fnFilterJs)
const appFiles = (pageManifest.pages['/_app'] ?? []).filter(fnFilterJs)
const fnMapRealPath = (dep: string) => `${distPath}/${dep}`
const allFilesReal = [...new Set([...pageFiles, ...appFiles])].map(
fnMapRealPath
)
const allFilesReal = unique(pageFiles, appFiles).map(fnMapRealPath)
const selfFilesReal = difference(
intersect(pageFiles, data.uniqueFiles),
data.commonFiles
// Find the files shared by the pages files and the unique files...
intersect(pageFiles, pageData.unique.files),
// but without the common files.
pageData.common.files
).map(fnMapRealPath)
const getSize = gzipSize ? fsStatGzip : fsStat
// Try to get the file size from the page data if available, otherwise do a
// raw compute.
const getCachedSize = async (file: string) => {
const key = file.slice(distPath.length + 1)
const size: number | undefined = stats.sizes.get(key)
// If the size wasn't in the stats bundle, then get it from the file
// directly.
if (typeof size !== 'number') {
return getSize(file)
}
return size
}
try {
// Doesn't use `Promise.all`, as we'd double compute duplicate files. This
// function is memoized, so the second one will instantly resolve.
const allFilesSize = sum(await Promise.all(allFilesReal.map(getSize)))
const selfFilesSize = sum(await Promise.all(selfFilesReal.map(getSize)))
const allFilesSize = sum(await Promise.all(allFilesReal.map(getCachedSize)))
const selfFilesSize = sum(
await Promise.all(selfFilesReal.map(getCachedSize))
)
return [selfFilesSize, allFilesSize]
} catch (_) {}
} catch {}
return [-1, -1]
}
@ -1126,7 +1289,7 @@ export function isServerComponentPage(
export async function copyTracedFiles(
dir: string,
distDir: string,
pageKeys: string[],
pageKeys: ReadonlyArray<string>,
tracingRoot: string,
serverConfig: { [key: string]: any },
middlewareManifest: MiddlewareManifest

View file

@ -19,7 +19,7 @@ import { CustomRoutes } from '../lib/load-custom-routes.js'
import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_WEBPACK,
@ -59,6 +59,7 @@ import browserslist from 'next/dist/compiled/browserslist'
import loadJsConfig from './load-jsconfig'
import { loadBindings } from './swc'
import { clientComponentRegex } from './webpack/loaders/utils'
import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plugin'
const watchOptions = Object.freeze({
aggregateTimeout: 5,
@ -572,7 +573,7 @@ export default async function getBaseWebpackConfig(
.replace(/\\/g, '/'),
...(config.experimental.appDir
? {
[CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]: dev
[CLIENT_STATIC_FILES_RUNTIME_MAIN_APP]: dev
? [
require.resolve(
`next/dist/compiled/@next/react-refresh-utils/dist/runtime`
@ -1718,6 +1719,10 @@ export default async function getBaseWebpackConfig(
minimized: true,
},
}),
!!config.experimental.appDir &&
hasServerComponents &&
isClient &&
new AppBuildManifestPlugin({ dev }),
hasServerComponents &&
(isClient
? new FlightManifestPlugin({

View file

@ -0,0 +1,98 @@
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
import {
APP_BUILD_MANIFEST,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
} from '../../../shared/lib/constants'
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
import { getEntrypointFiles } from './build-manifest-plugin'
import getAppRouteFromEntrypoint from '../../../server/get-app-route-from-entrypoint'
type Options = {
dev: boolean
}
export type AppBuildManifest = {
pages: Record<string, string[]>
}
const PLUGIN_NAME = 'AppBuildManifestPlugin'
export class AppBuildManifestPlugin {
private readonly dev: boolean
constructor(options: Options) {
this.dev = options.dev
}
public apply(compiler: any) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation: any, { normalModuleFactory }: any) => {
compilation.dependencyFactories.set(
(webpack as any).dependencies.ModuleDependency,
normalModuleFactory
)
compilation.dependencyTemplates.set(
(webpack as any).dependencies.ModuleDependency,
new (webpack as any).dependencies.NullDependency.Template()
)
}
)
compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets: any) => this.createAsset(assets, compilation)
)
})
}
private createAsset(assets: any, compilation: webpack5.Compilation) {
const manifest: AppBuildManifest = {
pages: {},
}
const systemEntrypoints = new Set<string>([
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
])
const mainFiles = new Set(
getEntrypointFiles(
compilation.entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN_APP)
)
)
for (const entrypoint of compilation.entrypoints.values()) {
if (!entrypoint.name) {
continue
}
if (systemEntrypoints.has(entrypoint.name)) {
continue
}
const pagePath = getAppRouteFromEntrypoint(entrypoint.name)
if (!pagePath) {
continue
}
const filesForPage = getEntrypointFiles(entrypoint)
manifest.pages[pagePath] = [...new Set([...mainFiles, ...filesForPage])]
}
const json = JSON.stringify(manifest, null, 2)
assets[APP_BUILD_MANIFEST] = new sources.RawSource(json)
}
}

View file

@ -6,7 +6,7 @@ import {
MIDDLEWARE_BUILD_MANIFEST,
CLIENT_STATIC_FILES_PATH,
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
@ -155,7 +155,7 @@ export default class BuildManifestPlugin {
assetMap.rootMainFiles = [
...new Set(
getEntrypointFiles(
entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT)
entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN_APP)
)
),
]
@ -192,7 +192,7 @@ export default class BuildManifestPlugin {
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
...(this.appDirEnabled ? [CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT] : []),
...(this.appDirEnabled ? [CLIENT_STATIC_FILES_RUNTIME_MAIN_APP] : []),
])
for (const entrypoint of compilation.entrypoints.values()) {

View file

@ -0,0 +1,17 @@
import matchBundle from './match-bundle'
// matches app/:path*.js
const APP_ROUTE_NAME_REGEX = /^app[/\\](.*)$/
export default function getAppRouteFromEntrypoint(entryFile: string) {
const pagePath = matchBundle(APP_ROUTE_NAME_REGEX, entryFile)
if (typeof pagePath === 'string' && !pagePath) {
return '/'
}
if (!pagePath) {
return null
}
return pagePath
}

View file

@ -1,22 +1,12 @@
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import getAppRouteFromEntrypoint from './get-app-route-from-entrypoint'
import matchBundle from './match-bundle'
// matches pages/:page*.js
const SERVER_ROUTE_NAME_REGEX = /^pages[/\\](.*)$/
// matches app/:path*.js
const APP_ROUTE_NAME_REGEX = /^app[/\\](.*)$/
// matches static/pages/:page*.js
const BROWSER_ROUTE_NAME_REGEX = /^static[/\\]pages[/\\](.*)$/
function matchBundle(regex: RegExp, input: string): string | null {
const result = regex.exec(input)
if (!result) {
return null
}
return getRouteFromAssetPath(`/${result[1]}`)
}
export default function getRouteFromEntrypoint(
entryFile: string,
app?: boolean
@ -28,8 +18,7 @@ export default function getRouteFromEntrypoint(
}
if (app) {
pagePath = matchBundle(APP_ROUTE_NAME_REGEX, entryFile)
if (typeof pagePath === 'string' && !pagePath) pagePath = '/'
pagePath = getAppRouteFromEntrypoint(entryFile)
if (pagePath) return pagePath
}

View file

@ -0,0 +1,14 @@
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
export default function matchBundle(
regex: RegExp,
input: string
): string | null {
const result = regex.exec(input)
if (!result) {
return null
}
return getRouteFromAssetPath(`/${result[1]}`)
}

View file

@ -7,6 +7,7 @@ export const PAGES_MANIFEST = 'pages-manifest.json'
export const APP_PATHS_MANIFEST = 'app-paths-manifest.json'
export const APP_PATH_ROUTES_MANIFEST = 'app-path-routes-manifest.json'
export const BUILD_MANIFEST = 'build-manifest.json'
export const APP_BUILD_MANIFEST = 'app-build-manifest.json'
export const EXPORT_MARKER = 'export-marker.json'
export const EXPORT_DETAIL = 'export-detail.json'
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
@ -47,7 +48,7 @@ export const MIDDLEWARE_REACT_LOADABLE_MANIFEST =
// static/runtime/main.js
export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `main`
export const CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT = `${CLIENT_STATIC_FILES_RUNTIME_MAIN}-app`
export const CLIENT_STATIC_FILES_RUNTIME_MAIN_APP = `${CLIENT_STATIC_FILES_RUNTIME_MAIN}-app`
// static/runtime/react-refresh.js
export const CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH = `react-refresh`
// static/runtime/amp.js

View file

@ -1,17 +1,19 @@
// remove (name) from pathname as it's not considered for routing
export function normalizeAppPath(pathname: string) {
let normalized = ''
const segments = pathname.split('/')
return pathname.split('/').reduce((acc, segment, index, segments) => {
// Empty segments are ignored.
if (!segment) {
return acc
}
segments.forEach((segment, index) => {
if (!segment) return
if (segment.startsWith('(') && segment.endsWith(')')) {
return
return acc
}
if (segment === 'page' && index === segments.length - 1) {
return
return acc
}
normalized += `/${segment}`
})
return normalized
return acc + `/${segment}`
}, '')
}

View file

@ -195,7 +195,9 @@ class UrlNode {
}
}
export function getSortedRoutes(normalizedPages: string[]): string[] {
export function getSortedRoutes(
normalizedPages: ReadonlyArray<string>
): string[] {
// First the UrlNode is created, and every UrlNode can have only 1 dynamic segment
// Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js
// Only 1 dynamic segment per nesting level