diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 5d757be91e85..78d94e230ded 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -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, } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index c4a0ced22e70..2d942a40070f 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -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> = [] 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() const ssgStaticFallbackPages = new Set() @@ -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 - ) + Object.entries(pageKeys) + .reduce>( + (acc, [key, files]) => { + if (!files) { + return acc + } - let isSsg = false - let isStatic = false - let isServerComponent = false - let isHybridAmp = false - let ssgPageRoutes: string[] | null = null + const pageType = key as keyof typeof pageKeys - const pagePath = pagePaths.find( - (p) => - p.startsWith(actualPage + '.') || - p.startsWith(actualPage + '/index.') - ) + for (const page of files) { + acc.push({ pageType, page }) + } - const pageRuntime = pagePath - ? ( - await getPageStaticInfo({ - pageFilePath: join(pagesDir, pagePath), - nextConfig: config, - }) - ).runtime - : undefined + 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 (hasServerComponents && pagePath) { - if (isServerComponentPage(config, pagePath)) { - isServerComponent = true + let isSsg = false + let isStatic = false + let isServerComponent = false + let isHybridAmp = false + let ssgPageRoutes: string[] | null = null + + const pagePath = + pageType === 'pages' + ? pagesPaths.find( + (p) => + p.startsWith(actualPage + '.') || + p.startsWith(actualPage + '/index.') + ) + : appPaths?.find((p) => p.startsWith(actualPage + '/page.')) + + 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 ( - !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 ( + // 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 (config.outputFileTracing) { + pageTraceIncludes.set( + page, + workerResult.traceIncludes || [] + ) + pageTraceExcludes.set( + page, + workerResult.traceExcludes || [] + ) + } - if ( - workerResult.isStatic === false && - (workerResult.isHybridAmp || workerResult.isAmpOnly) - ) { - hasSsrAmpPages = true - } + if ( + workerResult.isStatic === false && + (workerResult.isHybridAmp || workerResult.isAmpOnly) + ) { + hasSsrAmpPages = true + } - if (workerResult.isHybridAmp) { - isHybridAmp = true - hybridAmpPages.add(page) - } + if (workerResult.isHybridAmp) { + isHybridAmp = true + hybridAmpPages.add(page) + } - if (workerResult.isNextImageImported) { - isNextImageImported = true - } + if (workerResult.isNextImageImported) { + isNextImageImported = true + } - if (workerResult.hasStaticProps) { - ssgPages.add(page) - isSsg = true + if (workerResult.hasStaticProps) { + ssgPages.add(page) + isSsg = true - if ( - workerResult.prerenderRoutes && - workerResult.encodedPrerenderRoutes - ) { - additionalSsgPaths.set(page, workerResult.prerenderRoutes) - additionalSsgPathsEncoded.set( - page, + if ( + workerResult.prerenderRoutes && workerResult.encodedPrerenderRoutes - ) - ssgPageRoutes = workerResult.prerenderRoutes + ) { + 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 (workerResult.prerenderFallback === 'blocking') { - ssgBlockingFallbackPages.add(page) - } else if (workerResult.prerenderFallback === true) { - ssgStaticFallbackPages.add(page) + 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) + } } - } 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 ( + 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) { diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 32ab83145dd4..5cc24805b4e2 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -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 | undefined } = {} @@ -76,7 +79,10 @@ export interface PageInfo { } export async function printTreeView( - list: readonly string[], + lists: { + pages: ReadonlyArray + app?: ReadonlyArray + }, pageInfos: Map, 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) => + 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 + 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 - ? '└' - : '├' - - 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 - ? 'ℇ' - : 'λ' - - usedSymbols.add(symbol) - - 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) - : '' - : '', - ]) - - const uniqueCssFiles = - buildManifest.pages[item]?.filter( - (file) => file.endsWith('.css') && sizeData.uniqueFiles.includes(file) - ) || [] - - 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 }) - } + 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 symbol = + routerType === 'app' || item === '/_app' || item === '/_app.server' + ? ' ' + : pageInfo?.static + ? '○' + : pageInfo?.isSsg + ? '●' + : pageInfo?.runtime === SERVER_RUNTIME.edge + ? 'ℇ' + : 'λ' + + usedSymbols.add(symbol) + + if (pageInfo?.initialRevalidateSeconds) usedSymbols.add('ISR') + + 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') && + stats.router[routerType]?.unique.files.includes(file) + ) || [] + + if (uniqueCssFiles.length > 0) { + const contSymbol = i === arr.length - 1 ? ' ' : '├' + + 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, '')) - .sort(), - ...sharedCssFiles.map((e) => e.replace(buildId, '')).sort(), - ].forEach((fileName, index, { length }) => { - const innerSymbol = index === length - 1 ? '└' : '├' - const originalName = fileName.replace('', 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, '')) + .sort(), + ...sharedCssFiles.map((e) => e.replace(buildId, '')).sort(), + ].forEach((fileName, index, { length }) => { + const innerSymbol = index === length - 1 ? '└' : '├' + + const originalName = fileName.replace('', 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 + size: { + total: number + } +} + +type ComputeFilesManifest = { + unique: ComputeFilesGroup + common: ComputeFilesGroup +} + +type ComputeFilesManifestResult = { + router: { + pages: ComputeFilesManifest + app?: ComputeFilesManifest + } + sizes: Map } 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 -): Promise { +): Promise { 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() - 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, + key: string, + manifest: Record> + ) => { + 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 + expected: number + } + app?: { + each: Map + 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) - } - }) - }) + files.pages.expected++ + countBuildFiles(files.pages.each, key, manifests.build.pages) + } + + // Collect the build files form the app manifest. + if (manifests.app?.pages) { + files.app = { each: new Map(), 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() + + // For all of the files in the pages and app manifests, compute the file size + // at once. + + await Promise.all( + [ + ...new Set([ + ...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 commonFiles = [...files.entries()] - .filter(([, len]) => len === expected || len === Infinity) - .map(([f]) => f) - const uniqueFiles = [...files.entries()] - .filter(([, len]) => len === 1) - .map(([f]) => f) + const groupFiles = async (listing: { + each: Map + expected: number + }): Promise => { + const entries = [...listing.each.entries()] - 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 = [] - } + const shapeGroup = (group: [string, number][]): ComputeFilesGroup => + group.reduce( + (acc, [f]) => { + acc.files.push(f) - let uniqueStats: [string, number][] - try { - uniqueStats = await Promise.all( - uniqueFiles.map( - async (f) => - [f, await getSize(path.join(distPath, f))] as [string, number] + 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(main: T[] | Set, sub: T[] | Set): T[] { +export function unique(main: ReadonlyArray, sub: ReadonlyArray): T[] { + return [...new Set([...main, ...sub])] +} + +export function difference( + main: ReadonlyArray | ReadonlySet, + sub: ReadonlyArray | ReadonlySet +): T[] { const a = new Set(main) const b = new Set(sub) return [...a].filter((x) => !b.has(x)) } -function intersect(main: T[], sub: T[]): T[] { +/** + * Return an array of the items shared by both arrays. + */ +function intersect(main: ReadonlyArray, sub: ReadonlyArray): 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 { 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, tracingRoot: string, serverConfig: { [key: string]: any }, middlewareManifest: MiddlewareManifest diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 9de8fd598f2f..964b61f3587a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -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({ diff --git a/packages/next/build/webpack/plugins/app-build-manifest-plugin.ts b/packages/next/build/webpack/plugins/app-build-manifest-plugin.ts new file mode 100644 index 000000000000..f13b8901f4bc --- /dev/null +++ b/packages/next/build/webpack/plugins/app-build-manifest-plugin.ts @@ -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 +} + +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([ + 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) + } +} diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts index dac8c7cc6bc6..6f3d2ca34269 100644 --- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts @@ -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()) { diff --git a/packages/next/server/get-app-route-from-entrypoint.ts b/packages/next/server/get-app-route-from-entrypoint.ts new file mode 100644 index 000000000000..28e4870e709a --- /dev/null +++ b/packages/next/server/get-app-route-from-entrypoint.ts @@ -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 +} diff --git a/packages/next/server/get-route-from-entrypoint.ts b/packages/next/server/get-route-from-entrypoint.ts index 4a95b4c58b67..298c93d9ceca 100644 --- a/packages/next/server/get-route-from-entrypoint.ts +++ b/packages/next/server/get-route-from-entrypoint.ts @@ -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 } diff --git a/packages/next/server/match-bundle.ts b/packages/next/server/match-bundle.ts new file mode 100644 index 000000000000..7ee4deba7e02 --- /dev/null +++ b/packages/next/server/match-bundle.ts @@ -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]}`) +} diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index 6c381c89e7d3..9e97927eeed1 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -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 diff --git a/packages/next/shared/lib/router/utils/app-paths.ts b/packages/next/shared/lib/router/utils/app-paths.ts index 56edad8f58d7..bf69f05e0902 100644 --- a/packages/next/shared/lib/router/utils/app-paths.ts +++ b/packages/next/shared/lib/router/utils/app-paths.ts @@ -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}` + }, '') } diff --git a/packages/next/shared/lib/router/utils/sorted-routes.ts b/packages/next/shared/lib/router/utils/sorted-routes.ts index d3f0699bd1cf..973126c8e0e5 100644 --- a/packages/next/shared/lib/router/utils/sorted-routes.ts +++ b/packages/next/shared/lib/router/utils/sorted-routes.ts @@ -195,7 +195,9 @@ class UrlNode { } } -export function getSortedRoutes(normalizedPages: string[]): string[] { +export function getSortedRoutes( + normalizedPages: ReadonlyArray +): 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