diff --git a/packages/next/src/build/progress.ts b/packages/next/src/build/progress.ts index ef8c81b9e2fba..34dce6e7a2f74 100644 --- a/packages/next/src/build/progress.ts +++ b/packages/next/src/build/progress.ts @@ -68,19 +68,16 @@ export const createProgress = (total: number, label: string) => { } const isFinished = curProgress === total + const message = `${label} (${curProgress}/${total})` if (progressSpinner && !isFinished) { - progressSpinner.setText(`${label} (${curProgress}/${total})`) + progressSpinner.setText(message) } else { - if (progressSpinner) { - progressSpinner.stop() + progressSpinner?.stop() + if (isFinished) { + Log.event(message) + } else { + Log.info(`${message} ${process.stdout.isTTY ? '\n' : '\r'}`) } - console.log( - ` ${ - isFinished ? Log.prefixes.event : Log.prefixes.info - } ${label} (${curProgress}/${total}) ${ - isFinished ? '' : process.stdout.isTTY ? '\n' : '\r' - }` - ) } } } diff --git a/packages/next/src/build/spinner.ts b/packages/next/src/build/spinner.ts index d7fa635f4724b..49936409309bc 100644 --- a/packages/next/src/build/spinner.ts +++ b/packages/next/src/build/spinner.ts @@ -47,18 +47,18 @@ export default function createSpinner( console.warn = origWarn console.error = origError } - spinner.setText = (newText: string) => { + spinner.setText = (newText) => { text = newText prefixText = ` ${Log.prefixes.info} ${newText} ` spinner!.prefixText = prefixText return spinner! } - spinner.stop = (): ora.Ora => { + spinner.stop = () => { origStop() resetLog() return spinner! } - spinner.stopAndPersist = (): ora.Ora => { + spinner.stopAndPersist = () => { // Add \r at beginning to reset the current line of loading status text const suffixText = `\r ${Log.prefixes.event} ${text} ` if (spinner) { diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 515224ab5d812..66313819b3a32 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -481,7 +481,7 @@ export async function printTreeView( if (item === '/_app' || item === '/_app.server') { symbol = ' ' } else if (isEdgeRuntime(pageInfo?.runtime)) { - symbol = 'ℇ' + symbol = 'ƒ' } else if (pageInfo?.isPPR) { if ( // If the page has an empty prelude, then it's equivalent to a dynamic page @@ -490,7 +490,7 @@ export async function printTreeView( // since in this case we're able to partially prerender it (pageInfo.isDynamicAppRoute && !pageInfo.hasPostponed) ) { - symbol = 'λ' + symbol = 'ƒ' } else if (!pageInfo?.hasPostponed) { symbol = '○' } else { @@ -501,7 +501,7 @@ export async function printTreeView( } else if (pageInfo?.isSSG) { symbol = '●' } else { - symbol = 'λ' + symbol = 'ƒ' } usedSymbols.add(symbol) @@ -749,16 +749,7 @@ export async function printTreeView( '(Partial Prerender)', 'prerendered as static HTML with dynamic server-streamed content', ], - usedSymbols.has('λ') && [ - 'λ', - '(Dynamic)', - `server-rendered on demand using Node.js`, - ], - usedSymbols.has('ℇ') && [ - 'ℇ', - '(Edge Runtime)', - `server-rendered on demand using the Edge Runtime`, - ], + usedSymbols.has('ƒ') && ['ƒ', '(Dynamic)', `server-rendered on demand`], ].filter((x) => x) as [string, string, string][], { align: ['l', 'l', 'l'], diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 67b96f6e41d85..8f6f1d71415a1 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -18,6 +18,7 @@ import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' import type { SizeLimit } from '../../../../../types' import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until' import type { PAGE_TYPES } from '../../../../lib/page-types' +import type { NextRequestHint } from '../../../../server/web/adapter' export function getRender({ dev, @@ -151,23 +152,23 @@ export function getRender({ const handler = server.getRequestHandler() - return async function render(request: Request, event: NextFetchEvent) { + return async function render( + request: NextRequestHint, + event?: NextFetchEvent + ) { const extendedReq = new WebNextRequest(request) const extendedRes = new WebNextResponse() handler(extendedReq, extendedRes) const result = await extendedRes.toResponse() - if (event && event.waitUntil) { + if (event?.waitUntil) { const waitUntilPromise = internal_getCurrentFunctionWaitUntil() if (waitUntilPromise) { event.waitUntil(waitUntilPromise) } } - // fetchMetrics is attached to the web request that going through the server, - // wait for the handler result is ready and attach it back to the original request. - ;(request as any).fetchMetrics = extendedReq.fetchMetrics return result } } diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index f87e75a5ea25a..06d93e975a170 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -4,6 +4,7 @@ import type { AsyncLocalStorage } from 'async_hooks' import type { IncrementalCache } from '../lib/incremental-cache' import { createPrerenderState } from '../../server/app-render/dynamic-rendering' +import type { FetchMetric } from '../base-http' export type StaticGenerationContext = { urlPathname: string @@ -31,6 +32,8 @@ export type StaticGenerationContext = { */ // TODO: remove this when we resolve accessing the store outside the execution context store?: StaticGenerationStore + /** Fetch metrics attached in patch-fetch.ts */ + fetchMetrics?: FetchMetric[] } } diff --git a/packages/next/src/server/base-http/index.ts b/packages/next/src/server/base-http/index.ts index ad3454bd67463..af72b338cd9e9 100644 --- a/packages/next/src/server/base-http/index.ts +++ b/packages/next/src/server/base-http/index.ts @@ -11,7 +11,7 @@ export interface BaseNextRequestConfig { trailingSlash?: boolean | undefined } -export type FetchMetrics = Array<{ +export type FetchMetric = { url: string idx: number end: number @@ -20,11 +20,14 @@ export type FetchMetrics = Array<{ status: number cacheReason: string cacheStatus: 'hit' | 'miss' | 'skip' -}> +} + +export type FetchMetrics = Array export abstract class BaseNextRequest { protected _cookies: NextApiRequestCookies | undefined public abstract headers: IncomingHttpHeaders + public abstract fetchMetrics?: FetchMetric[] constructor(public method: string, public url: string, public body: Body) {} diff --git a/packages/next/src/server/base-http/node.ts b/packages/next/src/server/base-http/node.ts index f24c02f351167..1b7ed2953d06c 100644 --- a/packages/next/src/server/base-http/node.ts +++ b/packages/next/src/server/base-http/node.ts @@ -7,16 +7,18 @@ import type { NextApiRequestCookies } from '../api-utils' import { NEXT_REQUEST_META } from '../request-meta' import type { RequestMeta } from '../request-meta' -import { BaseNextRequest, BaseNextResponse } from './index' +import { BaseNextRequest, BaseNextResponse, type FetchMetric } from './index' import type { OutgoingHttpHeaders } from 'node:http' type Req = IncomingMessage & { [NEXT_REQUEST_META]?: RequestMeta cookies?: NextApiRequestCookies + fetchMetrics?: FetchMetric[] } export class NodeNextRequest extends BaseNextRequest { - public headers = this._req.headers; + public headers = this._req.headers + public fetchMetrics?: FetchMetric[] = this._req?.fetchMetrics; [NEXT_REQUEST_META]: RequestMeta = this._req[NEXT_REQUEST_META] || {} diff --git a/packages/next/src/server/base-http/web.ts b/packages/next/src/server/base-http/web.ts index a77ec0460b3df..58df722ffa20a 100644 --- a/packages/next/src/server/base-http/web.ts +++ b/packages/next/src/server/base-http/web.ts @@ -4,13 +4,14 @@ import type { FetchMetrics } from './index' import { toNodeOutgoingHttpHeaders } from '../web/utils' import { BaseNextRequest, BaseNextResponse } from './index' import { DetachedPromise } from '../../lib/detached-promise' +import type { NextRequestHint } from '../web/adapter' export class WebNextRequest extends BaseNextRequest { public request: Request public headers: IncomingHttpHeaders public fetchMetrics?: FetchMetrics - constructor(request: Request) { + constructor(request: NextRequestHint) { const url = new URL(request.url) super( @@ -19,6 +20,7 @@ export class WebNextRequest extends BaseNextRequest { request.clone().body ) this.request = request + this.fetchMetrics = request.fetchMetrics this.headers = {} for (const [name, value] of request.headers.entries()) { diff --git a/packages/next/src/server/future/route-modules/app-route/module.ts b/packages/next/src/server/future/route-modules/app-route/module.ts index e7cb2d0f4154f..2edb287eda3a7 100644 --- a/packages/next/src/server/future/route-modules/app-route/module.ts +++ b/packages/next/src/server/future/route-modules/app-route/module.ts @@ -391,7 +391,7 @@ export class AppRouteRouteModule extends RouteModule< `No response is returned from route handler '${this.resolvedPagePath}'. Ensure you return a \`Response\` or a \`NextResponse\` in all branches of your handler.` ) } - ;(context.renderOpts as any).fetchMetrics = + context.renderOpts.fetchMetrics = staticGenerationStore.fetchMetrics context.renderOpts.waitUntil = Promise.all( diff --git a/packages/next/src/server/lib/app-info-log.ts b/packages/next/src/server/lib/app-info-log.ts index 5e44794010866..cd8858447009c 100644 --- a/packages/next/src/server/lib/app-info-log.ts +++ b/packages/next/src/server/lib/app-info-log.ts @@ -12,7 +12,7 @@ export function logStartInfo({ appUrl, envInfo, expFeatureInfo, - maxExperimentalFeatures, + maxExperimentalFeatures = Infinity, }: { networkUrl: string | null appUrl: string | null @@ -21,31 +21,27 @@ export function logStartInfo({ maxExperimentalFeatures?: number }) { Log.bootstrap( - bold( - purple( - ` ${Log.prefixes.ready} Next.js ${process.env.__NEXT_VERSION}${ - process.env.TURBOPACK ? ' (turbo)' : '' - }` - ) - ) + `${bold( + purple(`${Log.prefixes.ready} Next.js ${process.env.__NEXT_VERSION}`) + )}${process.env.TURBOPACK ? ' (turbo)' : ''}` ) if (appUrl) { - Log.bootstrap(` - Local: ${appUrl}`) + Log.bootstrap(`- Local: ${appUrl}`) } if (networkUrl) { - Log.bootstrap(` - Network: ${networkUrl}`) + Log.bootstrap(`- Network: ${networkUrl}`) } - if (envInfo?.length) Log.bootstrap(` - Environments: ${envInfo.join(', ')}`) + if (envInfo?.length) Log.bootstrap(`- Environments: ${envInfo.join(', ')}`) if (expFeatureInfo?.length) { - Log.bootstrap(` - Experiments (use with caution):`) - // only show maximum 3 flags + Log.bootstrap(`- Experiments (use with caution):`) + // only show a maximum number of flags for (const exp of expFeatureInfo.slice(0, maxExperimentalFeatures)) { - Log.bootstrap(` · ${exp}`) + Log.bootstrap(` · ${exp}`) } - /* ${expFeatureInfo.length - 3} more */ - if (expFeatureInfo.length > 3 && maxExperimentalFeatures) { - Log.bootstrap(` · ...`) + /* indicate if there are more than the maximum shown no. flags */ + if (expFeatureInfo.length > maxExperimentalFeatures) { + Log.bootstrap(` · ...`) } } diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 35eccced36f1e..676bb261ac1c8 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -14,6 +14,7 @@ import { } from '../../lib/constants' import * as Log from '../../build/output/log' import { trackDynamicFetch } from '../app-render/dynamic-rendering' +import type { FetchMetric } from '../base-http' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -146,38 +147,23 @@ export function addImplicitTags(staticGenerationStore: StaticGenerationStore) { function trackFetchMetric( staticGenerationStore: StaticGenerationStore, - ctx: { - url: string - status: number - method: string - cacheReason: string - cacheStatus: 'hit' | 'miss' | 'skip' - start: number - } + ctx: Omit ) { if (!staticGenerationStore) return - if (!staticGenerationStore.fetchMetrics) { - staticGenerationStore.fetchMetrics = [] - } - const dedupeFields = ['url', 'status', 'method'] + staticGenerationStore.fetchMetrics ??= [] + + const dedupeFields = ['url', 'status', 'method'] as const // don't add metric if one already exists for the fetch if ( - staticGenerationStore.fetchMetrics.some((metric) => { - return dedupeFields.every( - (field) => (metric as any)[field] === (ctx as any)[field] - ) - }) + staticGenerationStore.fetchMetrics.some((metric) => + dedupeFields.every((field) => metric[field] === ctx[field]) + ) ) { return } staticGenerationStore.fetchMetrics.push({ - url: ctx.url, - cacheStatus: ctx.cacheStatus, - cacheReason: ctx.cacheReason, - status: ctx.status, - method: ctx.method, - start: ctx.start, + ...ctx, end: Date.now(), idx: staticGenerationStore.nextFetchId || 0, }) diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 293bd5dc04eda..48620758f0ef4 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -259,6 +259,8 @@ export async function startServer( maxExperimentalFeatures: 3, }) + Log.event(`Starting...`) + try { const cleanup = () => { debug('start-server process cleanup') diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 68068bc989128..3334287973af1 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -12,7 +12,11 @@ import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plu import type RenderResult from './render-result' import type { FetchEventResult } from './web/types' import type { PrerenderManifest } from '../build' -import type { BaseNextRequest, BaseNextResponse } from './base-http' +import type { + BaseNextRequest, + BaseNextResponse, + FetchMetric, +} from './base-http' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { Params } from '../shared/lib/router/utils/route-matcher' @@ -1091,7 +1095,7 @@ export default class NextNodeServer extends BaseServer { const shouldTruncateUrl = !loggingFetchesConfig?.fullUrl if (this.renderOpts.dev) { - const { bold, green, yellow, red, gray, white } = + const { blue, green, yellow, red, gray, white } = require('../lib/picocolors') as typeof import('../lib/picocolors') const _req = req as NodeNextRequest | IncomingMessage const _res = res as NodeNextResponse | ServerResponse @@ -1112,33 +1116,28 @@ export default class NextNodeServer extends BaseServer { return } const reqEnd = Date.now() - const fetchMetrics = (normalizedReq as any).fetchMetrics || [] + const fetchMetrics = normalizedReq.fetchMetrics || [] const reqDuration = reqEnd - reqStart - const getDurationStr = (duration: number) => { - let durationStr = duration.toString() - - if (duration < 500) { - durationStr = green(duration + 'ms') - } else if (duration < 2000) { - durationStr = yellow(duration + 'ms') - } else { - durationStr = red(duration + 'ms') - } - return durationStr + const statusColor = (status?: number) => { + if (!status || status < 200) return white + else if (status < 300) return green + else if (status < 400) return blue + else if (status < 500) return yellow + return red } - if (Array.isArray(fetchMetrics) && fetchMetrics.length) { - if (enabledVerboseLogging) { - writeStdoutLine( - `${white(bold(req.method || 'GET'))} ${req.url} ${ - res.statusCode - } in ${getDurationStr(reqDuration)}` - ) - } + const color = statusColor(res.statusCode) + const method = req.method || 'GET' + writeStdoutLine( + `${color(method)} ${color(req.url ?? '')} ${ + res.statusCode + } in ${reqDuration}ms` + ) + if (fetchMetrics.length && enabledVerboseLogging) { const calcNestedLevel = ( - prevMetrics: any[], + prevMetrics: FetchMetric[], start: number ): string => { let nestedLevel = 0 @@ -1154,7 +1153,7 @@ export default class NextNodeServer extends BaseServer { nestedLevel += 1 } } - return nestedLevel === 0 ? ' ' : ' │ '.repeat(nestedLevel) + return nestedLevel === 0 ? ' ' : ' │ '.repeat(nestedLevel) } for (let i = 0; i < fetchMetrics.length; i++) { @@ -1162,17 +1161,16 @@ export default class NextNodeServer extends BaseServer { let { cacheStatus, cacheReason } = metric let cacheReasonStr = '' + let cacheColor const duration = metric.end - metric.start - if (cacheStatus === 'hit') { - cacheStatus = green('HIT') - } else if (cacheStatus === 'skip') { - cacheStatus = yellow('SKIP') + cacheColor = green + } else { + cacheColor = yellow + const status = cacheStatus === 'skip' ? 'skipped' : 'missed' cacheReasonStr = gray( - `Cache missed reason: (${white(cacheReason)})` + `Cache ${status} reason: (${white(cacheReason)})` ) - } else { - cacheStatus = yellow('MISS') } let url = metric.url @@ -1199,45 +1197,29 @@ export default class NextNodeServer extends BaseServer { truncatedSearch } - if (enabledVerboseLogging) { - const newLineLeadingChar = '│' - const nestedIndent = calcNestedLevel( + const status = cacheColor(`(cache ${cacheStatus})`) + const newLineLeadingChar = '│' + const nestedIndent = calcNestedLevel( + fetchMetrics.slice(0, i + 1), + metric.start + ) + + writeStdoutLine( + `${newLineLeadingChar}${nestedIndent}${white( + metric.method + )} ${white(url)} ${metric.status} in ${duration}ms ${status}` + ) + if (cacheReasonStr) { + const nextNestedIndent = calcNestedLevel( fetchMetrics.slice(0, i + 1), metric.start ) writeStdoutLine( - ` ${`${newLineLeadingChar}${nestedIndent}${white( - bold(metric.method) - )} ${gray(url)} ${metric.status} in ${getDurationStr( - duration - )} (cache: ${cacheStatus})`}` + `${newLineLeadingChar}${nextNestedIndent}${newLineLeadingChar} ${cacheReasonStr}` ) - if (cacheReasonStr) { - const nextNestedIndent = calcNestedLevel( - fetchMetrics.slice(0, i + 1), - metric.start - ) - writeStdoutLine( - ' ' + - newLineLeadingChar + - nextNestedIndent + - ' ' + - newLineLeadingChar + - ' ' + - cacheReasonStr - ) - } } } - } else { - if (enabledVerboseLogging) { - writeStdoutLine( - `${white(bold(req.method || 'GET'))} ${req.url} ${ - res.statusCode - } in ${getDurationStr(reqDuration)}` - ) - } } origRes.off('close', reqCallback) } @@ -1910,7 +1892,7 @@ export default class NextNodeServer extends BaseServer { }) if (result.fetchMetrics) { - ;(params.req as any).fetchMetrics = result.fetchMetrics + params.req.fetchMetrics = result.fetchMetrics } if (!params.res.statusCode || params.res.statusCode < 400) { diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 994f3be57f0e5..1a54ae7c5f649 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -1,4 +1,4 @@ -import type { NextMiddleware, RequestData, FetchEventResult } from './types' +import type { RequestData, FetchEventResult } from './types' import type { RequestInit } from './spec-extension/request' import type { PrerenderManifest } from '../../build' import { PageSignatureError } from './error' @@ -20,7 +20,7 @@ import { getTracer } from '../lib/trace/tracer' import type { TextMapGetter } from 'next/dist/compiled/@opentelemetry/api' import { MiddlewareSpan } from '../lib/trace/constants' -class NextRequestHint extends NextRequest { +export class NextRequestHint extends NextRequest { sourcePage: string fetchMetrics?: FetchEventResult['fetchMetrics'] @@ -52,7 +52,7 @@ const headersGetter: TextMapGetter = { } export type AdapterOptions = { - handler: NextMiddleware + handler: (req: NextRequestHint, event: NextFetchEvent) => Promise page: string request: RequestData IncrementalCache?: typeof import('../lib/incremental-cache').IncrementalCache diff --git a/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts b/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts index 1657be6e9ee19..23c54b12787d1 100644 --- a/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts +++ b/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts @@ -1,11 +1,12 @@ import { findPort, waitFor } from 'next-test-utils' import http from 'http' import { outdent } from 'outdent' -import { FileRef, createNext } from 'e2e-utils' +import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils' describe('app-fetch-deduping', () => { - if ((global as any).isNextStart) { + if (isNextStart) { describe('during static generation', () => { + const { next } = nextTestSetup({ files: __dirname, skipStart: true }) let externalServerPort: number let externalServer: http.Server let requests = [] @@ -35,28 +36,27 @@ describe('app-fetch-deduping', () => { afterAll(() => externalServer.close()) it('dedupes requests amongst static workers when experimental.staticWorkerRequestDeduping is enabled', async () => { - const next = await createNext({ - files: new FileRef(__dirname), - env: { TEST_SERVER_PORT: `${externalServerPort}` }, - nextConfig: { + await next.patchFileFast( + 'next.config.js', + `module.exports = { + env: { TEST_SERVER_PORT: "${externalServerPort}" }, experimental: { - staticWorkerRequestDeduping: true, - }, - }, - }) - + staticWorkerRequestDeduping: true + } + }` + ) + await next.build() expect(requests.length).toBe(1) - - await next.destroy() }) }) - } else if ((global as any).isNextDev) { + } else if (isNextDev) { describe('during next dev', () => { - it('should dedupe requests called from the same component', async () => { - const next = await createNext({ - files: new FileRef(__dirname), - }) + const { next } = nextTestSetup({ files: __dirname }) + function invocation(cliOutput: string): number { + return cliOutput.match(/Route Handler invoked/g).length + } + it('should dedupe requests called from the same component', async () => { await next.patchFile( 'app/test/page.tsx', outdent` @@ -71,28 +71,23 @@ describe('app-fetch-deduping', () => { const time = await getTime() return

{time}

- } - ` + }` ) await next.render('/test') - let count = next.cliOutput.split('Starting...').length - 1 - expect(count).toBe(1) - - await next.destroy() + expect(invocation(next.cliOutput)).toBe(1) + await next.stop() }) it('should dedupe pending revalidation requests', async () => { - const next = await createNext({ - files: new FileRef(__dirname), - }) - + await next.start() + const revalidate = 5 await next.patchFile( 'app/test/page.tsx', outdent` async function getTime() { - const res = await fetch("http://localhost:${next.appPort}/api/time", { next: { revalidate: 5 } }) + const res = await fetch("http://localhost:${next.appPort}/api/time", { next: { revalidate: ${revalidate} } }) return res.text() } @@ -102,27 +97,19 @@ describe('app-fetch-deduping', () => { const time = await getTime() return

{time}

- } - ` + }` ) await next.render('/test') - let count = next.cliOutput.split('Starting...').length - 1 - expect(count).toBe(1) - - const outputIndex = next.cliOutput.length + expect(invocation(next.cliOutput)).toBe(1) // wait for the revalidation to finish - await waitFor(6000) + await waitFor(revalidate * 1000 + 1000) await next.render('/test') - count = - next.cliOutput.slice(outputIndex).split('Starting...').length - 1 - expect(count).toBe(1) - - await next.destroy() + expect(invocation(next.cliOutput)).toBe(2) }) }) } else { diff --git a/test/e2e/app-dir/app-fetch-deduping/app/api/time/route.ts b/test/e2e/app-dir/app-fetch-deduping/app/api/time/route.ts index a864c4b8ae08d..f58b85ce5455f 100644 --- a/test/e2e/app-dir/app-fetch-deduping/app/api/time/route.ts +++ b/test/e2e/app-dir/app-fetch-deduping/app/api/time/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' export async function GET() { - console.log('Starting...') + console.log('Route Handler invoked') return NextResponse.json({ time: Date.now() }) } diff --git a/test/e2e/app-dir/logging/fetch-logging.test.ts b/test/e2e/app-dir/logging/fetch-logging.test.ts index 604e0daa08f6e..71417e2f63aeb 100644 --- a/test/e2e/app-dir/logging/fetch-logging.test.ts +++ b/test/e2e/app-dir/logging/fetch-logging.test.ts @@ -2,249 +2,251 @@ import path from 'path' import fs from 'fs' import stripAnsi from 'strip-ansi' import { retry } from 'next-test-utils' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' + +const cahceReasonRe = /Cache (missed|skipped) reason: / + +interface ParsedLog { + method: string + url: string + statusCode: number + responseTime: number + cache: string +} function parseLogsFromCli(cliOutput: string) { const logs = stripAnsi(cliOutput) .split('\n') - .filter((log) => log.includes('Cache missed reason') || log.includes('GET')) + .filter((log) => cahceReasonRe.test(log) || log.includes('GET')) - return logs.reduce((parsedLogs, log) => { - if (log.includes('Cache missed reason')) { - // cache miss reason - const reasonSegment = log.split('Cache missed reason: ', 2)[1].trim() + return logs.reduce((parsedLogs, log) => { + if (cahceReasonRe.test(log)) { + // cache miss/skip reason + // Example of `log`: "│ │ Cache skipped reason: (cache: no-cache)" + const reasonSegment = log.split(cahceReasonRe, 3)[2].trim() const reason = reasonSegment.slice(1, -1) parsedLogs[parsedLogs.length - 1].cache = reason } else { // request info const trimmedLog = log.replace(/^[^a-zA-Z]+/, '') - const parts = trimmedLog.split(' ', 5) - const method = parts[0] - const url = parts[1] - const statusCode = parseInt(parts[2]) - const responseTime = parseInt(parts[4]) + const [method, url, statusCode, responseTime] = trimmedLog.split(' ', 5) - const parsedLog = { + parsedLogs.push({ method, url, - statusCode, - responseTime, + statusCode: parseInt(statusCode, 10), + responseTime: parseInt(responseTime, 10), cache: undefined, - } - parsedLogs.push(parsedLog) + }) } return parsedLogs - }, [] as any[]) + }, []) } -createNextDescribe( - 'app-dir - logging', - { +describe('app-dir - logging', () => { + const { next, isNextDev } = nextTestSetup({ skipDeployment: true, files: __dirname, - }, - ({ next, isNextDev }) => { - function runTests({ - withFetchesLogging, - withFullUrlFetches = false, - }: { - withFetchesLogging: boolean - withFullUrlFetches?: boolean - }) { - if (withFetchesLogging) { - it('should only log requests in development mode', async () => { + }) + function runTests({ + withFetchesLogging, + withFullUrlFetches = false, + }: { + withFetchesLogging: boolean + withFullUrlFetches?: boolean + }) { + if (withFetchesLogging) { + it('should only log requests in development mode', async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/default-cache') + + await retry(() => { + const logs = stripAnsi(next.cliOutput.slice(outputIndex)) + if (isNextDev) { + expect(logs).toContain('GET /default-cache 200') + } else { + expect(logs).not.toContain('GET /default-cache 200') + } + }) + }) + + if (isNextDev) { + it("should log 'skip' cache status with a reason when cache: 'no-cache' is used", async () => { const outputIndex = next.cliOutput.length await next.fetch('/default-cache') await retry(() => { - const logs = stripAnsi(next.cliOutput.slice(outputIndex)) - const hasLogs = logs.includes('GET /default-cache 200') - - expect(isNextDev ? hasLogs : !hasLogs).toBe(true) - }) - }) + const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) - if (isNextDev) { - it("should log 'skip' cache status with a reason when cache: 'no-cache' is used", async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/default-cache') + const logEntry = logs.find((log) => + log.url.includes('api/random?no-cache') + ) - await retry(() => { - const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) + expect(logs.some((log) => log.url.includes('..'))).toBe( + !withFullUrlFetches + ) - const logEntry = logs.find((log) => - log.url.includes('api/random?no-cache') - ) - - expect(logs.some((log) => log.url.includes('..'))).toBe( - !withFullUrlFetches - ) - - expect(logEntry?.cache).toBe('cache: no-cache') - }) + expect(logEntry?.cache).toBe('cache: no-cache') }) + }) - it("should log 'skip' cache status with a reason when revalidate: 0 is used", async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/default-cache') - await retry(() => { - const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) + it("should log 'skip' cache status with a reason when revalidate: 0 is used", async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/default-cache') + await retry(() => { + const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) - const logEntry = logs.find((log) => - log.url.includes('api/random?revalidate-0') - ) + const logEntry = logs.find((log) => + log.url.includes('api/random?revalidate-0') + ) - expect(logEntry?.cache).toBe('revalidate: 0') - }) + expect(logEntry?.cache).toBe('revalidate: 0') }) + }) - it("should log 'skip' cache status with a reason when the browser indicates caching should be ignored", async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/default-cache', { - headers: { 'Cache-Control': 'no-cache' }, - }) - await retry(() => { - const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) - - const logEntry = logs.find((log) => - log.url.includes('api/random?auto-cache') - ) - - expect(logEntry?.cache).toBe( - 'cache-control: no-cache (hard refresh)' - ) - }) - }) - - it('should log requests with correct indentation', async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/default-cache') - - await retry(() => { - const logs = stripAnsi(next.cliOutput.slice(outputIndex)) - const hasLogs = - logs.includes(' GET /default-cache') && - logs.includes(' │ GET ') && - logs.includes(' │ │ GET ') && - logs.includes(' │ │ Cache missed reason') - - expect(hasLogs).toBe(true) - }) + it("should log 'skip' cache status with a reason when the browser indicates caching should be ignored", async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/default-cache', { + headers: { 'Cache-Control': 'no-cache' }, }) + await retry(() => { + const logs = parseLogsFromCli(next.cliOutput.slice(outputIndex)) - it('should show cache reason of noStore when use with fetch', async () => { - const logLength = next.cliOutput.length - await next.fetch('/no-store') + const logEntry = logs.find((log) => + log.url.includes('api/random?auto-cache') + ) - await retry(() => { - const output = stripAnsi(next.cliOutput.slice(logLength)) - expect(output).toContain('Cache missed reason: (noStore call)') - }) + expect(logEntry?.cache).toBe( + 'cache-control: no-cache (hard refresh)' + ) }) + }) - it('should respect request.init.cache when use with fetch input is instance', async () => { - const logLength = next.cliOutput.length - await next.fetch('/fetch-no-store') - - await retry(() => { - const output = stripAnsi(next.cliOutput.slice(logLength)) - expect(output).toContain('Cache missed reason: (cache: no-store)') - }) - }) - } - } else { - // No fetches logging enabled - it('should not log fetch requests at all', async () => { + it('should log requests with correct indentation', async () => { const outputIndex = next.cliOutput.length await next.fetch('/default-cache') await retry(() => { const logs = stripAnsi(next.cliOutput.slice(outputIndex)) - expect(logs).not.toContain('GET /default-cache 200') + expect(logs).toContain(' GET /default-cache') + expect(logs).toContain(' │ GET ') + expect(logs).toContain(' │ │ Cache skipped reason') + expect(logs).toContain(' │ │ GET ') }) }) - } - if (isNextDev) { - it('should not contain trailing word page for app router routes', async () => { + it('should show cache reason of noStore when use with fetch', async () => { const logLength = next.cliOutput.length - await next.fetch('/') + await next.fetch('/no-store') await retry(() => { const output = stripAnsi(next.cliOutput.slice(logLength)) - expect(output).toContain('/') - expect(output).not.toContain('/page') + expect(output).toContain('Cache skipped reason: (noStore call)') }) }) - it('should not contain metadata internal segments for dynamic metadata routes', async () => { + it('should respect request.init.cache when use with fetch input is instance', async () => { const logLength = next.cliOutput.length - await next.fetch('/dynamic/big/icon') + await next.fetch('/fetch-no-store') await retry(() => { const output = stripAnsi(next.cliOutput.slice(logLength)) - expect(output).toContain('/dynamic/[slug]/icon') - expect(output).not.toContain('/(group)') - expect(output).not.toContain('[[...__metadata_id__]]') - expect(output).not.toContain('/route') + expect(output).toContain('Cache skipped reason: (cache: no-store)') }) }) } + } else { + // No fetches logging enabled + it('should not log fetch requests at all', async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/default-cache') + + await retry(() => { + const logs = stripAnsi(next.cliOutput.slice(outputIndex)) + expect(logs).not.toContain('GET /default-cache 200') + }) + }) } - describe('with fetches verbose logging', () => { - runTests({ withFetchesLogging: true, withFullUrlFetches: true }) - }) + if (isNextDev) { + it('should not contain trailing word page for app router routes', async () => { + const logLength = next.cliOutput.length + await next.fetch('/') - describe('with fetches default logging', () => { - const curNextConfig = fs.readFileSync( - path.join(__dirname, 'next.config.js'), - { encoding: 'utf-8' } - ) - beforeAll(async () => { - await next.stop() - await next.patchFile( - 'next.config.js', - curNextConfig.replace('fullUrl: true', 'fullUrl: false') - ) - await next.start() - }) - afterAll(async () => { - await next.patchFile('next.config.js', curNextConfig) + await retry(() => { + const output = stripAnsi(next.cliOutput.slice(logLength)) + expect(output).toContain('/') + expect(output).not.toContain('/page') + }) }) - runTests({ withFetchesLogging: true, withFullUrlFetches: false }) - }) + it('should not contain metadata internal segments for dynamic metadata routes', async () => { + const logLength = next.cliOutput.length + await next.fetch('/dynamic/big/icon') - describe('with verbose logging for edge runtime', () => { - beforeAll(async () => { - await next.stop() - const layoutContent = await next.readFile('app/layout.js') - await next.patchFile( - 'app/layout.js', - layoutContent + `\nexport const runtime = 'edge'` - ) - await next.start() + await retry(() => { + const output = stripAnsi(next.cliOutput.slice(logLength)) + expect(output).toContain('/dynamic/[slug]/icon') + expect(output).not.toContain('/(group)') + expect(output).not.toContain('[[...__metadata_id__]]') + expect(output).not.toContain('/route') + }) }) + } + } - runTests({ withFetchesLogging: false }) + describe('with fetches verbose logging', () => { + runTests({ withFetchesLogging: true, withFullUrlFetches: true }) + }) + + describe('with fetches default logging', () => { + const curNextConfig = fs.readFileSync( + path.join(__dirname, 'next.config.js'), + { encoding: 'utf-8' } + ) + beforeAll(async () => { + await next.stop() + await next.patchFile( + 'next.config.js', + curNextConfig.replace('fullUrl: true', 'fullUrl: false') + ) + await next.start() + }) + afterAll(async () => { + await next.patchFile('next.config.js', curNextConfig) }) - describe('with default logging', () => { - const curNextConfig = fs.readFileSync( - path.join(__dirname, 'next.config.js'), - { encoding: 'utf-8' } + runTests({ withFetchesLogging: true, withFullUrlFetches: false }) + }) + + describe('with verbose logging for edge runtime', () => { + beforeAll(async () => { + await next.stop() + const layoutContent = await next.readFile('app/layout.js') + await next.patchFile( + 'app/layout.js', + layoutContent + `\nexport const runtime = 'edge'` ) - beforeAll(async () => { - await next.stop() - await next.deleteFile('next.config.js') - await next.start() - }) - afterAll(async () => { - await next.patchFile('next.config.js', curNextConfig) - }) + await next.start() + }) - runTests({ withFetchesLogging: false }) + runTests({ withFetchesLogging: false }) + }) + + describe('with default logging', () => { + const curNextConfig = fs.readFileSync( + path.join(__dirname, 'next.config.js'), + { encoding: 'utf-8' } + ) + beforeAll(async () => { + await next.stop() + await next.deleteFile('next.config.js') + await next.start() }) - } -) + afterAll(async () => { + await next.patchFile('next.config.js', curNextConfig) + }) + + runTests({ withFetchesLogging: false }) + }) +}) diff --git a/test/e2e/app-dir/revalidate-dynamic/revalidate-dynamic.test.ts b/test/e2e/app-dir/revalidate-dynamic/revalidate-dynamic.test.ts index c084d67a216e9..6554566673e60 100644 --- a/test/e2e/app-dir/revalidate-dynamic/revalidate-dynamic.test.ts +++ b/test/e2e/app-dir/revalidate-dynamic/revalidate-dynamic.test.ts @@ -8,8 +8,8 @@ createNextDescribe( ({ next, isNextStart }) => { if (isNextStart) { it('should correctly mark a route handler that uses revalidateTag as dynamic', async () => { - expect(next.cliOutput).toContain('λ /api/revalidate-path') - expect(next.cliOutput).toContain('λ /api/revalidate-tag') + expect(next.cliOutput).toContain('ƒ /api/revalidate-path') + expect(next.cliOutput).toContain('ƒ /api/revalidate-tag') }) } diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts index 24b518a447a55..becf353bdc4d1 100644 --- a/test/e2e/switchable-runtime/index.test.ts +++ b/test/e2e/switchable-runtime/index.test.ts @@ -585,17 +585,17 @@ describe('Switchable runtime', () => { const expectedOutputLines = splitLines(` ┌ /_app ├ ○ /404 - ├ ℇ /api/hello - ├ λ /api/node - ├ ℇ /edge - ├ ℇ /edge-rsc + ├ ƒ /api/hello + ├ ƒ /api/node + ├ ƒ /edge + ├ ƒ /edge-rsc ├ ○ /node ├ ● /node-rsc ├ ● /node-rsc-isr ├ ● /node-rsc-ssg - ├ λ /node-rsc-ssr + ├ ƒ /node-rsc-ssr ├ ● /node-ssg - ├ λ /node-ssr + ├ ƒ /node-ssr └ ○ /static `) diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 6871742cd5ad7..d4fc1e682a3b0 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -267,7 +267,7 @@ describe('Build Output', () => { }) expect(stdout).toMatch(/\/ (.* )?\d{1,} B/) - expect(stdout).toMatch(/λ \/404 (.* )?\d{1,} B/) + expect(stdout).toMatch(/ƒ \/404 (.* )?\d{1,} B/) expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/) expect(stdout).toMatch(/ chunks\/main-[0-9a-z]{16}\.js [ 0-9.]* kB/) expect(stdout).toMatch( @@ -295,7 +295,7 @@ describe('Build Output', () => { stdout: true, }) expect(stdout).toContain('○ /404') - expect(stdout).not.toContain('λ /_error') + expect(stdout).not.toContain('ƒ /_error') expect(stdout).not.toContain('') }) }) diff --git a/test/integration/custom-error/test/index.test.js b/test/integration/custom-error/test/index.test.js index 1673086e8d118..e344553a4cd59 100644 --- a/test/integration/custom-error/test/index.test.js +++ b/test/integration/custom-error/test/index.test.js @@ -96,8 +96,8 @@ describe('Custom _error', () => { afterAll(() => killApp(app)) it('should not contain /_error in build output', async () => { - expect(buildOutput).toMatch(/λ .*?\/404/) - expect(buildOutput).not.toMatch(/λ .*?\/_error/) + expect(buildOutput).toMatch(/ƒ .*?\/404/) + expect(buildOutput).not.toMatch(/ƒ .*?\/_error/) }) runTests()