Skip to content

Commit

Permalink
fix: handle error case for output: export in next dev (#47768)
Browse files Browse the repository at this point in the history
  • Loading branch information
styfle committed Apr 6, 2023
1 parent 4fbbb62 commit 86cb8ec
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 139 deletions.
11 changes: 0 additions & 11 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,6 @@ export async function isPageStatic({
isrFlushToDisk,
maxMemoryCacheSize,
incrementalCacheHandlerPath,
nextConfigOutput,
}: {
page: string
distDir: string
Expand Down Expand Up @@ -1498,16 +1497,6 @@ export async function isPageStatic({
{}
)

if (nextConfigOutput === 'export') {
if (!appConfig.dynamic || appConfig.dynamic === 'auto') {
appConfig.dynamic = 'error'
} else if (appConfig.dynamic === 'force-dynamic') {
throw new Error(
`export const dynamic = "force-dynamic" on page "${page}" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export`
)
}
}

if (appConfig.dynamic === 'force-dynamic') {
appConfig.revalidate = 0
}
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/cli/next-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ const nextBuild: CliCommand = (argv) => {
(err.code === 'INVALID_RESOLVE_ALIAS' ||
err.code === 'WEBPACK_ERRORS' ||
err.code === 'BUILD_OPTIMIZATION_FAILED' ||
err.code === 'NEXT_EXPORT_ERROR' ||
err.code === 'NEXT_STATIC_GEN_BAILOUT' ||
err.code === 'EDGE_RUNTIME_UNSUPPORTED_API')
) {
printAndExit(`> ${err.message}`)
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/cli/next-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ const nextExport: CliCommand = (argv) => {
nextExportCliSpan.stop()
printAndExit(`Export successful. Files written to ${options.outdir}`, 0)
})
.catch((err: unknown) => {
.catch((err: any) => {
nextExportCliSpan.stop()
if (err instanceof ExportError) {
if (err instanceof ExportError || err.code === 'NEXT_EXPORT_ERROR') {
Log.error(err.message)
} else {
console.error(err)
Expand Down
20 changes: 17 additions & 3 deletions packages/next/src/client/components/static-generation-bailout.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { DynamicServerError } from './hooks-server-context'
import { staticGenerationAsyncStorage } from './static-generation-async-storage'

export function staticGenerationBailout(reason: string): boolean | never {
class StaticGenBailoutError extends Error {
code = 'NEXT_STATIC_GEN_BAILOUT'
}

export type StaticGenerationBailout = (
reason: string,
opts?: { dynamic?: string; link?: string }
) => boolean | never

export const staticGenerationBailout: StaticGenerationBailout = (
reason,
opts
) => {
const staticGenerationStore = staticGenerationAsyncStorage.getStore()

if (staticGenerationStore?.forceStatic) {
return true
}

if (staticGenerationStore?.dynamicShouldError) {
throw new Error(
`Page with \`dynamic = "error"\` couldn't be rendered statically because it used \`${reason}\``
const { dynamic = 'error', link } = opts || {}
const suffix = link ? ` See more info here: ${link}` : ''
throw new StaticGenBailoutError(
`Page with \`dynamic = "${dynamic}"\` couldn't be rendered statically because it used \`${reason}\`.${suffix}`
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ const createProgress = (total: number, label: string) => {
}

export class ExportError extends Error {
type = 'ExportError'
code = 'NEXT_EXPORT_ERROR'
}

export interface ExportOptions {
Expand Down
37 changes: 34 additions & 3 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
Segment,
} from './types'
import type { StaticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage'
import type { StaticGenerationBailout } from '../../client/components/static-generation-bailout'
import type { RequestAsyncStorage } from '../../client/components/request-async-storage'
import type { MetadataItems } from '../../lib/metadata/resolve-metadata'

Expand Down Expand Up @@ -166,6 +167,7 @@ export async function renderToHTMLOrFlight(
dev,
nextFontManifest,
supportsDynamicHTML,
nextConfigOutput,
} = renderOpts

const clientReferenceManifest = renderOpts.clientReferenceManifest!
Expand Down Expand Up @@ -217,6 +219,8 @@ export async function renderToHTMLOrFlight(
ComponentMod.staticGenerationAsyncStorage
const requestAsyncStorage: RequestAsyncStorage =
ComponentMod.requestAsyncStorage
const staticGenerationBailout: StaticGenerationBailout =
ComponentMod.staticGenerationBailout

// we wrap the render in an AsyncLocalStorage context
const wrappedRender = async () => {
Expand Down Expand Up @@ -645,15 +649,33 @@ export async function renderToHTMLOrFlight(
? [DefaultNotFound]
: []

if (typeof layoutOrPageMod?.dynamic === 'string') {
let dynamic = layoutOrPageMod?.dynamic

if (nextConfigOutput === 'export') {
if (!dynamic || dynamic === 'auto') {
dynamic = 'error'
} else if (dynamic === 'force-dynamic') {
staticGenerationStore.forceDynamic = true
staticGenerationStore.dynamicShouldError = true
staticGenerationBailout(`output: export`, {
dynamic,
link: 'https://nextjs.org/docs/advanced-features/static-html-export',
})
}
}

if (typeof dynamic === 'string') {
// the nested most config wins so we only force-static
// if it's configured above any parent that configured
// otherwise
if (layoutOrPageMod.dynamic === 'error') {
if (dynamic === 'error') {
staticGenerationStore.dynamicShouldError = true
} else if (dynamic === 'force-dynamic') {
staticGenerationStore.forceDynamic = true
staticGenerationBailout(`force-dynamic`, { dynamic })
} else {
staticGenerationStore.dynamicShouldError = false
if (layoutOrPageMod.dynamic === 'force-static') {
if (dynamic === 'force-static') {
staticGenerationStore.forceStatic = true
} else {
staticGenerationStore.forceStatic = false
Expand Down Expand Up @@ -1460,6 +1482,15 @@ export async function renderToHTMLOrFlight(

return result
} catch (err: any) {
if (
err.code === 'NEXT_STATIC_GEN_BAILOUT' ||
err.message?.includes(
'https://nextjs.org/docs/advanced-features/static-html-export'
)
) {
// Ensure that "next dev" prints the red error overlay
throw err
}
if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) {
warn(
`Entire page ${pathname} deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ export class AppRouteRouteModule extends RouteModule<
// The dynamic property is set to force-dynamic, so we should
// force the page to be dynamic.
staticGenerationStore.forceDynamic = true
this.staticGenerationBailout(`dynamic = 'force-dynamic'`)
this.staticGenerationBailout(`force-dynamic`, {
dynamic: this.dynamic,
})
break
case 'force-static':
// The dynamic property is set to force-static, so we should
Expand Down
4 changes: 2 additions & 2 deletions test/development/acceptance-app/dynamic-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ createNextDescribe(

await session.hasRedbox(true)
console.log(await session.getRedboxDescription())
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"Error: Page with \`dynamic = \\"error\\"\` couldn't be rendered statically because it used \`cookies\`"`
expect(await session.getRedboxDescription()).toBe(
`Error: Page with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`.`
)

await cleanup()
Expand Down
Loading

0 comments on commit 86cb8ec

Please sign in to comment.