mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
feat: build lifecycle hooks - afterProductionCompile (#77345)
Add a `runAfterProductionCompile` hook in next.config.js that executes after production build compilation finishes, but before running post-compilation tasks like type checking and static page generation. This hook provides access to project metadata including the project directory and build output directory, making it useful for third-party to collect build outputs like sourcemaps.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import type { NextConfigComplete } from '../server/config-shared'
|
||||
import type { Span } from '../trace'
|
||||
|
||||
import * as Log from './output/log'
|
||||
import createSpinner from './spinner'
|
||||
import isError from '../lib/is-error'
|
||||
import type { Telemetry } from '../telemetry/storage'
|
||||
import { EVENT_BUILD_FEATURE_USAGE } from '../telemetry/events/build'
|
||||
|
||||
// TODO: refactor this to account for more compiler lifecycle events
|
||||
// such as beforeProductionBuild, but for now this is the only one that is needed
|
||||
export async function runAfterProductionCompile({
|
||||
config,
|
||||
buildSpan,
|
||||
telemetry,
|
||||
metadata,
|
||||
}: {
|
||||
config: NextConfigComplete
|
||||
buildSpan: Span
|
||||
telemetry: Telemetry
|
||||
metadata: {
|
||||
projectDir: string
|
||||
distDir: string
|
||||
}
|
||||
}): Promise<void> {
|
||||
const run = config.compiler.runAfterProductionCompile
|
||||
if (!run) {
|
||||
return
|
||||
}
|
||||
telemetry.record([
|
||||
{
|
||||
eventName: EVENT_BUILD_FEATURE_USAGE,
|
||||
payload: {
|
||||
featureName: 'runAfterProductionCompile',
|
||||
invocationCount: 1,
|
||||
},
|
||||
},
|
||||
])
|
||||
const afterBuildSpinner = createSpinner(
|
||||
'Running next.config.js provided runAfterProductionCompile'
|
||||
)
|
||||
|
||||
try {
|
||||
const startTime = performance.now()
|
||||
await buildSpan
|
||||
.traceChild('after-production-compile')
|
||||
.traceAsyncFn(async () => {
|
||||
await run(metadata)
|
||||
})
|
||||
const duration = performance.now() - startTime
|
||||
const formattedDuration = `${Math.round(duration)}ms`
|
||||
Log.event(`Completed runAfterProductionCompile in ${formattedDuration}`)
|
||||
} catch (err) {
|
||||
// Handle specific known errors differently if needed
|
||||
if (isError(err)) {
|
||||
Log.error(`Failed to run runAfterProductionCompile: ${err.message}`)
|
||||
}
|
||||
|
||||
throw err
|
||||
} finally {
|
||||
afterBuildSpinner?.stop()
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,7 @@ import { populateStaticEnv } from '../lib/static-env'
|
||||
import { durationToString } from './duration-to-string'
|
||||
import { traceGlobals } from '../trace/shared'
|
||||
import { extractNextErrorCode } from '../lib/error-telemetry-utils'
|
||||
import { runAfterProductionCompile } from './after-production-compile'
|
||||
|
||||
type Fallback = null | boolean | string
|
||||
|
||||
@@ -1583,6 +1584,15 @@ export default async function build(
|
||||
)
|
||||
}
|
||||
}
|
||||
await runAfterProductionCompile({
|
||||
config,
|
||||
buildSpan: nextBuildSpan,
|
||||
telemetry,
|
||||
metadata: {
|
||||
projectDir: dir,
|
||||
distDir,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// For app directory, we run type checking after build.
|
||||
|
||||
@@ -263,6 +263,10 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
|
||||
}),
|
||||
]),
|
||||
define: z.record(z.string(), z.string()).optional(),
|
||||
runAfterProductionCompile: z
|
||||
.function()
|
||||
.returns(z.promise(z.void()))
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
compress: z.boolean().optional(),
|
||||
|
||||
@@ -1055,6 +1055,22 @@ export interface NextConfig extends Record<string, any> {
|
||||
* replaced with the respective values.
|
||||
*/
|
||||
define?: Record<string, string>
|
||||
|
||||
/**
|
||||
* A hook function that executes after production build compilation finishes,
|
||||
* but before running post-compilation tasks such as type checking and
|
||||
* static page generation.
|
||||
*/
|
||||
runAfterProductionCompile?: (metadata: {
|
||||
/**
|
||||
* The root directory of the project
|
||||
*/
|
||||
projectDir: string
|
||||
/**
|
||||
* The build output directory (defaults to `.next`)
|
||||
*/
|
||||
distDir: string
|
||||
}) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1200,6 +1216,7 @@ export const defaultConfig: NextConfig = {
|
||||
keepAlive: true,
|
||||
},
|
||||
logging: {},
|
||||
compiler: {},
|
||||
expireTime: process.env.NEXT_PRIVATE_CDN_CONSUMED_SWR_CACHE_CONTROL
|
||||
? undefined
|
||||
: 31536000, // one year
|
||||
|
||||
@@ -195,6 +195,7 @@ export type EventBuildFeatureUsage = {
|
||||
| 'webpackPlugins'
|
||||
| UseCacheTrackerKey
|
||||
| 'turbopackPersistentCaching'
|
||||
| 'runAfterProductionCompile'
|
||||
invocationCount: number
|
||||
}
|
||||
export function eventBuildFeatureUsage(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs/promises'
|
||||
|
||||
export async function after({
|
||||
distDir,
|
||||
projectDir,
|
||||
}: {
|
||||
distDir: string
|
||||
projectDir: string
|
||||
}) {
|
||||
try {
|
||||
console.log(`Using distDir: ${distDir}`)
|
||||
console.log(`Using projectDir: ${projectDir}`)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
const files = await fs.readdir(distDir, { recursive: true })
|
||||
console.log(`Total files in ${distDir} folder: ${files.length}`)
|
||||
} catch (err) {
|
||||
console.error(`Error reading ${distDir} directory:`, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, World!</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export async function after({
|
||||
distDir,
|
||||
projectDir,
|
||||
}: {
|
||||
distDir: string
|
||||
projectDir: string
|
||||
}) {
|
||||
throw new Error('error after production build')
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { nextTestSetup } from 'e2e-utils'
|
||||
import { findAllTelemetryEvents } from 'next-test-utils'
|
||||
|
||||
describe('build-lifecycle-hooks', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: {
|
||||
NEXT_TELEMETRY_DEBUG: '1',
|
||||
},
|
||||
})
|
||||
|
||||
it('should run runAfterProductionCompile', async () => {
|
||||
const output = next.cliOutput
|
||||
|
||||
expect(output).toContain('')
|
||||
expect(output).toContain(`Using distDir: ${next.testDir}/.next`)
|
||||
expect(output).toContain(`Using projectDir: ${next.testDir}`)
|
||||
expect(output).toContain(`Total files in ${next.testDir}/.next folder:`)
|
||||
expect(output).toContain('Completed runAfterProductionCompile in')
|
||||
|
||||
// Ensure telemetry event is recorded
|
||||
const events = findAllTelemetryEvents(output, 'NEXT_BUILD_FEATURE_USAGE')
|
||||
expect(events).toContainEqual({
|
||||
featureName: 'runAfterProductionCompile',
|
||||
invocationCount: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow throwing error in runAfterProductionCompile', async () => {
|
||||
try {
|
||||
await next.stop()
|
||||
await next.patchFile('next.config.ts', (content) => {
|
||||
return content.replace(
|
||||
`import { after } from './after'`,
|
||||
`import { after } from './bad-after'`
|
||||
)
|
||||
})
|
||||
|
||||
const getCliOutput = next.getCliOutputFromHere()
|
||||
await next.build()
|
||||
expect(getCliOutput()).toContain('error after production build')
|
||||
expect(getCliOutput()).not.toContain(
|
||||
'Completed runAfterProductionCompile in'
|
||||
)
|
||||
} finally {
|
||||
await next.patchFile('next.config.ts', (content) => {
|
||||
return content.replace(
|
||||
`import { after } from './bad-after'`,
|
||||
`import { after } from './after'`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextConfig } from 'next'
|
||||
import { after } from './after'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
compiler: {
|
||||
runAfterProductionCompile: async ({ distDir, projectDir }) => {
|
||||
await after({ distDir, projectDir })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
Reference in New Issue
Block a user