diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 31abf214a35dc..355627ee4fe25 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -649,12 +649,20 @@ const staticWorkerExposedMethods = [ type StaticWorker = typeof import('./worker') & Worker export function createStaticWorker( config: NextConfigComplete, - onActivity?: () => void + progress?: { + run: () => void + clear: () => void + } ): StaticWorker { return new Worker(staticWorkerPath, { logger: Log, numWorkers: getNumberOfWorkers(config), - onActivity, + onActivity: () => { + progress?.run() + }, + onActivityAbort: () => { + progress?.clear() + }, forkOptions: { env: process.env, }, @@ -1516,7 +1524,7 @@ export default async function build( remainingRampup-- sema.release() } - progress() + progress.run() } })() ) diff --git a/packages/next/src/build/progress.ts b/packages/next/src/build/progress.ts index 34dce6e7a2f74..ab2ee2ae18492 100644 --- a/packages/next/src/build/progress.ts +++ b/packages/next/src/build/progress.ts @@ -47,7 +47,7 @@ export const createProgress = (total: number, label: string) => { }, }) - return () => { + const run = () => { curProgress++ // Make sure we only log once @@ -80,4 +80,22 @@ export const createProgress = (total: number, label: string) => { } } } + + const clear = () => { + if ( + progressSpinner && + // Ensure only reset and clear once to avoid set operation overflow in ora + progressSpinner.isSpinning + ) { + progressSpinner.prefixText = '\r' + progressSpinner.text = '\r' + progressSpinner.clear() + progressSpinner.stop() + } + } + + return { + run, + clear, + } } diff --git a/packages/next/src/build/spinner.ts b/packages/next/src/build/spinner.ts index 49936409309bc..57378baf20d33 100644 --- a/packages/next/src/build/spinner.ts +++ b/packages/next/src/build/spinner.ts @@ -33,9 +33,20 @@ export default function createSpinner( const origStopAndPersist = spinner.stopAndPersist.bind(spinner) const logHandle = (method: any, args: any[]) => { - origStop() + // 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) { + // Reset the current running spinner to empty line by `\r` + spinner.prefixText = '\r' + spinner.text = '\r' + spinner.clear() + origStop() + } method(...args) - spinner!.start() + if (spinner && isInProgress) { + spinner.start() + } } console.log = (...args: any) => logHandle(origLog, args) diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index 3976343b19673..65a99e9407491 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -539,7 +539,7 @@ async function exportPage( } } catch (err) { console.error( - `\nError occurred prerendering page "${input.path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` + `Error occurred prerendering page "${input.path}". Read more: https://nextjs.org/docs/messages/prerender-error` ) // bailoutToCSRError errors should not leak to the user as they are not actionable; they're diff --git a/packages/next/src/lib/worker.ts b/packages/next/src/lib/worker.ts index 6726af61ff079..aeeeaa1d59338 100644 --- a/packages/next/src/lib/worker.ts +++ b/packages/next/src/lib/worker.ts @@ -4,6 +4,8 @@ import { getParsedNodeOptionsWithoutInspect, formatNodeOptions, } from '../server/lib/utils' +import { Transform } from 'stream' + type FarmOptions = ConstructorParameters[1] const RESTARTED = Symbol('restarted') @@ -24,6 +26,7 @@ export class Worker { options: FarmOptions & { timeout?: number onActivity?: () => void + onActivityAbort?: () => void onRestart?: (method: string, args: any[], attempts: number) => void logger?: Pick exposedMethods: ReadonlyArray @@ -106,6 +109,26 @@ export class Worker { } } + let aborted = false + const onActivityAbort = () => { + if (!aborted) { + options.onActivityAbort?.() + aborted = true + } + } + + // Listen to the worker's stdout and stderr, if there's any thing logged, abort the activity first + const abortActivityStreamOnLog = new Transform({ + transform(_chunk, _encoding, callback) { + onActivityAbort() + callback() + }, + }) + // Stop the activity if there's any output from the worker + this._worker.getStdout().pipe(abortActivityStreamOnLog) + this._worker.getStderr().pipe(abortActivityStreamOnLog) + + // Pipe the worker's stdout and stderr to the parent process this._worker.getStdout().pipe(process.stdout) this._worker.getStderr().pipe(process.stderr) }