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:
parent
b15a976e11
commit
f5cab2f515
12 changed files with 827 additions and 494 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
17
packages/next/server/get-app-route-from-entrypoint.ts
Normal file
17
packages/next/server/get-app-route-from-entrypoint.ts
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
14
packages/next/server/match-bundle.ts
Normal file
14
packages/next/server/match-bundle.ts
Normal 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]}`)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
}, '')
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue