diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 4b9e62a333b9b3..9d1ffa4c9f811d 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -210,7 +210,7 @@ import { turbopackBuild } from './turbopack-build' import { isFileSystemCacheEnabledForBuild } from '../shared/lib/turbopack/utils' import { inlineStaticEnv } from '../lib/inline-static-env' import { populateStaticEnv } from '../lib/static-env' -import { durationToString, hrtimeDurationToString } from './duration-to-string' +import { durationToString } from './duration-to-string' import { traceGlobals } from '../trace/shared' import { extractNextErrorCode } from '../lib/error-telemetry-utils' import { runAfterProductionCompile } from './after-production-compile' @@ -1876,7 +1876,6 @@ export default async function build( traceMemoryUsage('Finished type checking', nextBuildSpan) } - const collectingPageDataStart = process.hrtime() const postCompileSpinner = createSpinner('Collecting page data') const buildManifestPath = path.join(distDir, BUILD_MANIFEST) @@ -2438,10 +2437,6 @@ export default async function build( }) if (postCompileSpinner) { - const collectingPageDataEnd = process.hrtime(collectingPageDataStart) - postCompileSpinner.setText( - `Collecting page data in ${hrtimeDurationToString(collectingPageDataEnd)}` - ) postCompileSpinner.stopAndPersist() } traceMemoryUsage('Finished collecting page data', nextBuildSpan) @@ -3950,15 +3945,6 @@ export default async function build( .traceAsyncFn(() => writeManifest(routesManifestPath, routesManifest)) } - const finalizingPageOptimizationStart = process.hrtime() - const postBuildSpinner = createSpinner('Finalizing page optimization') - let buildTracesSpinner - let buildTracesStart - if (buildTracesPromise) { - buildTracesStart = process.hrtime() - buildTracesSpinner = createSpinner('Collecting build traces') - } - // ensure the worker is not left hanging worker.end() @@ -4104,17 +4090,15 @@ export default async function build( }) } + let buildTracesSpinner + if (buildTracesPromise) { + buildTracesSpinner = createSpinner('Collecting build traces') + } + await buildTracesPromise if (buildTracesSpinner) { - if (buildTracesStart) { - const buildTracesEnd = process.hrtime(buildTracesStart) - buildTracesSpinner.setText( - `Collecting build traces in ${hrtimeDurationToString(buildTracesEnd)}` - ) - } buildTracesSpinner.stopAndPersist() - buildTracesSpinner = undefined } if (isCompileMode) { @@ -4124,6 +4108,7 @@ export default async function build( } if (config.output === 'export') { + const spinner = createSpinner('Outputting static export') await nextBuildSpan .traceChild('output-export-full-static-export') .traceAsyncFn(async () => { @@ -4136,6 +4121,8 @@ export default async function build( appDirOnly ) }) + + spinner.stopAndPersist() } // This should come after output: export handling but before @@ -4143,6 +4130,7 @@ export default async function build( // not be allowed if an adapter with onBuildComplete is configured const adapterPath = config.experimental.adapterPath if (adapterPath) { + const spinner = createSpinner('Finalizing build with adapter') await nextBuildSpan .traceChild('adapter-handle-build-complete') .traceAsyncFn(async () => { @@ -4167,9 +4155,11 @@ export default async function build( requiredServerFiles: requiredServerFilesManifest.files, }) }) + spinner.stopAndPersist() } if (config.output === 'standalone') { + const spinner = createSpinner('Generating standalone output') await nextBuildSpan .traceChild('output-standalone') .traceAsyncFn(async () => { @@ -4188,19 +4178,9 @@ export default async function build( appDir ) }) + spinner.stopAndPersist() } - if (postBuildSpinner) { - const finalizingPageOptimizationEnd = process.hrtime( - finalizingPageOptimizationStart - ) - postBuildSpinner.setText( - `Finalizing page optimization in ${hrtimeDurationToString(finalizingPageOptimizationEnd)}` - ) - postBuildSpinner.stopAndPersist() - } - console.log() - if (debugOutput) { nextBuildSpan .traceChild('print-custom-routes') diff --git a/packages/next/src/build/progress.ts b/packages/next/src/build/progress.ts index 273b6e5cc8cd93..1ccc1b18459f47 100644 --- a/packages/next/src/build/progress.ts +++ b/packages/next/src/build/progress.ts @@ -1,5 +1,3 @@ -import * as Log from '../build/output/log' -import { hrtimeDurationToString } from './duration-to-string' import createSpinner from './spinner' function divideSegments(number: number, segments: number): number[] { @@ -16,8 +14,6 @@ function divideSegments(number: number, segments: number): number[] { } export const createProgress = (total: number, label: string) => { - const progressStart = process.hrtime() - const segments = divideSegments(total, 4) if (total === 0) { @@ -57,7 +53,7 @@ export const createProgress = (total: number, label: string) => { // - per fully generated segment, or // - per minute // when not showing the spinner - if (!progressSpinner) { + if (!process.stdout.isTTY) { currentSegmentCount++ if (currentSegmentCount === currentSegmentTotal) { @@ -72,22 +68,15 @@ export const createProgress = (total: number, label: string) => { const isFinished = curProgress === total const message = `${label} (${curProgress}/${total})` - if (progressSpinner && !isFinished) { - progressSpinner.setText(message) - } else { - progressSpinner?.stop() - if (isFinished) { - const progressEnd = process.hrtime(progressStart) - Log.event(`${message} in ${hrtimeDurationToString(progressEnd)}`) - } else { - Log.info(`${message} ${process.stdout.isTTY ? '\n' : '\r'}`) - } + progressSpinner.setText(message) + if (isFinished) { + progressSpinner.stopAndPersist() } } const clear = () => { if ( - progressSpinner && + process.stdout.isTTY && // Ensure only reset and clear once to avoid set operation overflow in ora progressSpinner.isSpinning ) { diff --git a/packages/next/src/build/spinner.ts b/packages/next/src/build/spinner.ts index 57378baf20d332..40637034aee883 100644 --- a/packages/next/src/build/spinner.ts +++ b/packages/next/src/build/spinner.ts @@ -1,5 +1,6 @@ import ora from 'next/dist/compiled/ora' import * as Log from './output/log' +import { hrtimeBigIntDurationToString } from './duration-to-string' const dotsSpinner = { frames: ['.', '..', '...'], @@ -11,12 +12,11 @@ export default function createSpinner( options: ora.Options = {}, logFn: (...data: any[]) => void = console.log ) { - let spinner: undefined | (ora.Ora & { setText: (text: string) => void }) - let prefixText = ` ${Log.prefixes.info} ${text} ` + const progressStart = process.hrtime.bigint() if (process.stdout.isTTY) { - spinner = ora({ + let spinner = ora({ text: undefined, prefixText, spinner: dotsSpinner, @@ -35,8 +35,8 @@ export default function createSpinner( const logHandle = (method: any, args: any[]) => { // Enter a new line before logging new message, to avoid // the new message shows up right after the spinner in the same line. - const isInProgress = spinner?.isSpinning - if (spinner && isInProgress) { + const isInProgress = spinner.isSpinning + if (isInProgress) { // Reset the current running spinner to empty line by `\r` spinner.prefixText = '\r' spinner.text = '\r' @@ -44,7 +44,7 @@ export default function createSpinner( origStop() } method(...args) - if (spinner && isInProgress) { + if (isInProgress) { spinner.start() } } @@ -61,29 +61,46 @@ export default function createSpinner( spinner.setText = (newText) => { text = newText prefixText = ` ${Log.prefixes.info} ${newText} ` - spinner!.prefixText = prefixText - return spinner! + spinner.prefixText = prefixText + return spinner } spinner.stop = () => { origStop() resetLog() - return spinner! + return spinner } spinner.stopAndPersist = () => { - // Add \r at beginning to reset the current line of loading status text - const suffixText = `\r ${Log.prefixes.event} ${text} ` - if (spinner) { - spinner.text = suffixText - } else { - logFn(suffixText) - } + const duration = process.hrtime.bigint() - progressStart + text = `${text} in ${hrtimeBigIntDurationToString(duration)}` + prefixText = ` ${Log.prefixes.info} ${text} ` + spinner.prefixText = prefixText origStopAndPersist() resetLog() - return spinner! + return spinner } - } else if (prefixText || text) { - logFn(prefixText ? prefixText + '...' : text) - } + return spinner + } else { + text = ` ${Log.prefixes.info} ${text} ` + logFn(text) - return spinner + // @ts-ignore + let spinner = { + isSpinning: false, + prefixText: '', + text: '', + clear: '', + setText(newText: string) { + text = ` ${Log.prefixes.info} ${newText}` + logFn(text) + }, + stop: () => spinner, + stopAndPersist: () => { + const duration = process.hrtime.bigint() - progressStart + logFn(`${text} in ${hrtimeBigIntDurationToString(duration)}`) + return spinner! + }, + } as ora.Ora & { setText: (text: string) => void } + + return spinner + } } diff --git a/packages/next/src/build/type-check.ts b/packages/next/src/build/type-check.ts index 6fcd75e339ab28..dbcd546edd7ac4 100644 --- a/packages/next/src/build/type-check.ts +++ b/packages/next/src/build/type-check.ts @@ -7,7 +7,6 @@ import { Worker } from '../lib/worker' import createSpinner from './spinner' import { eventTypeCheckCompleted } from '../telemetry/events' import isError from '../lib/is-error' -import { hrtimeDurationToString } from './duration-to-string' /** * typescript will be loaded in "next/lib/verify-typescript-setup" and @@ -127,11 +126,7 @@ export async function startTypeChecking({ ) if (typeCheckingSpinner) { - typeCheckingSpinner.stop() - - createSpinner( - `Finished TypeScript${ignoreTypeScriptErrors ? ' config validation' : ''} in ${hrtimeDurationToString(typeCheckEnd)}` - )?.stopAndPersist() + typeCheckingSpinner.stopAndPersist() } if (!ignoreTypeScriptErrors && verifyResult) { diff --git a/test/e2e/app-dir/cache-components-errors/cache-components-console-patch.test.ts b/test/e2e/app-dir/cache-components-errors/cache-components-console-patch.test.ts index 60befcba762553..1726b39997ad82 100644 --- a/test/e2e/app-dir/cache-components-errors/cache-components-console-patch.test.ts +++ b/test/e2e/app-dir/cache-components-errors/cache-components-console-patch.test.ts @@ -62,16 +62,16 @@ describe('Cache Components Errors', () => { if (isTurbopack) { expect(output).toMatchInlineSnapshot(` "[] This is a console log from a server component page - [] This is a console log from a server component page - []" + [] This is a console log from a server component page" `) } else { expect(output).toMatchInlineSnapshot(` - "[] This is a console log from a server component page - [] This is a console log from a server component page - [] Collecting build traces ... - []" - `) + "[] This is a console log from a server component page + [] This is a console log from a server component page + [] Collecting build traces ... + [] ✓ Collecting build traces in 3.1s + []" + `) } }) } diff --git a/test/integration/polyfills/test/index.test.js b/test/integration/polyfills/test/index.test.js index 800a827c1bf110..ae57bc35ef9f1d 100644 --- a/test/integration/polyfills/test/index.test.js +++ b/test/integration/polyfills/test/index.test.js @@ -51,8 +51,8 @@ describe('Polyfills', () => { it('should contain generated page count in output', async () => { expect(output).toContain('Generating static pages (0/5)') expect(output).toContain('Generating static pages (5/5)') - // we should only have 1 segment and the initial message logged out - expect(output.match(/Generating static pages/g).length).toBe(5) + // we should have 4 segments, the initial message, and the final one with time + expect(output.match(/Generating static pages/g).length).toBe(6) }) } ) diff --git a/test/production/app-dir/build-output-debug/index.test.ts b/test/production/app-dir/build-output-debug/index.test.ts index a16d9d3936b93e..37789d654247ba 100644 --- a/test/production/app-dir/build-output-debug/index.test.ts +++ b/test/production/app-dir/build-output-debug/index.test.ts @@ -12,33 +12,36 @@ describe('next build --debug', () => { output = stripAnsi(next.cliOutput) }) - const str = ` - - -Redirects -┌ source: /:path+/ -├ destination: /:path+ -└ permanent: true + it('should log Redirects above Route(app)', async () => { + let data = output + .slice(output.indexOf('Redirects\n'), output.indexOf('○ (Static)')) + .trim() -┌ source: /redirects -├ destination: / -└ permanent: true + expect(data).toMatchInlineSnapshot(` + "Redirects + ┌ source: /:path+/ + ├ destination: /:path+ + └ permanent: true + ┌ source: /redirects + ├ destination: / + └ permanent: true -Headers -┌ source: / -└ headers: - └ x-custom-headers: headers + Headers + ┌ source: / + └ headers: + └ x-custom-headers: headers -Rewrites -┌ source: /rewrites -└ destination: / + Rewrites + ┌ source: /rewrites + └ destination: / -Route (app)` - it('should log Redirects above Route(app)', async () => { - expect(output).toContain(str) + Route (app) + ┌ ○ / + └ ○ /_not-found" + `) }) }) diff --git a/test/production/build-spinners/index.test.ts b/test/production/build-spinners/index.test.ts index 65032a25d7107f..a32616013699a6 100644 --- a/test/production/build-spinners/index.test.ts +++ b/test/production/build-spinners/index.test.ts @@ -144,7 +144,6 @@ describe('build-spinners', () => { let optimizedBuildIdx = -1 let collectingPageDataIdx = -1 let generatingStaticIdx = -1 - let finalizingOptimization = -1 // order matters so we check output from end to start for (let i = output.length - 1; i--; i >= 0) { @@ -174,24 +173,15 @@ describe('build-spinners', () => { ) { generatingStaticIdx = i } - - if ( - finalizingOptimization === -1 && - line.includes('Finalizing page optimization') - ) { - finalizingOptimization = i - } } expect(compiledIdx).not.toBe(-1) expect(optimizedBuildIdx).not.toBe(-1) expect(collectingPageDataIdx).not.toBe(-1) expect(generatingStaticIdx).not.toBe(-1) - expect(finalizingOptimization).not.toBe(-1) expect(optimizedBuildIdx).toBeLessThan(compiledIdx) expect(compiledIdx).toBeLessThan(collectingPageDataIdx) expect(collectingPageDataIdx).toBeLessThan(generatingStaticIdx) - expect(generatingStaticIdx).toBeLessThan(finalizingOptimization) }) }) diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts index e61da9616a1d3b..1425719ec44dca 100644 --- a/test/production/pages-dir/production/test/index.test.ts +++ b/test/production/pages-dir/production/test/index.test.ts @@ -116,8 +116,8 @@ describe('Production Usage', () => { expect(next.cliOutput).toContain( `Generating static pages (${pageCount}/${pageCount})` ) - // we should only have 4 segments and the initial message logged out - expect(next.cliOutput.match(/Generating static pages/g).length).toBe(5) + // we should have 4 segments, the initial message, and the final one with time + expect(next.cliOutput.match(/Generating static pages/g).length).toBe(6) }) it('should output traces', async () => {