Skip to content

Commit

Permalink
Add support for output: export config (#46744)
Browse files Browse the repository at this point in the history
## Background

In the early days, `next export` was created when Next.js was SSR-only in order to statically export your pages for self hosting where no server was available. However, around the time `getStaticProps()` and `getStaticPaths()` were introduced, Next.js began [automatically generating static pages](https://nextjs.org/docs/advanced-features/automatic-static-optimization) (SSG first and SSR opt-in) during `next build`. This meant there were very few reasons to use `next export` and it started to become a stale feature.

## Problem We Need To Solve

Users targeting `next export` currently have a really bad experience. They start a new project and use all the features Next.js has to offer because they all features work with `next dev`. Then when development is finished and it comes time to deploy, running `next build && next export` will fail with errors for [unsupported features](https://nextjs.org/docs/advanced-features/static-html-export#unsupported-features).

## Solution

This PR introduces a new configuration option, `output: 'export'`, to indicate that the user intends to run `next export`.

With this change, Next.js can fail fast during `next dev` if any [unsupported features](https://nextjs.org/docs/advanced-features/static-html-export#unsupported-features) are used, thereby improving developer experience with instant feedback.


```js
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
}

module.exports = nextConfig
```
  • Loading branch information
styfle committed Mar 4, 2023
1 parent ef685e8 commit 25efdfa
Show file tree
Hide file tree
Showing 20 changed files with 581 additions and 8 deletions.
18 changes: 16 additions & 2 deletions docs/advanced-features/static-html-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ If you're looking to build a hybrid site where only _some_ pages are prerendered

## `next export`

Update your build script in `package.json` to use `next export`:
Update your `next.config.js` file to include `output: "export"` like the following:

```js
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
}

module.exports = nextConfig
```

Update your scripts in `package.json` file to include `next export` like the following:

```json
"scripts": {
Expand Down Expand Up @@ -59,7 +72,8 @@ Features that require a Node.js server, or dynamic logic that cannot be computed
- [Headers](/docs/api-reference/next.config.js/headers.md)
- [Middleware](/docs/middleware.md)
- [Incremental Static Regeneration](/docs/basic-features/data-fetching/incremental-static-regeneration.md)
- [`fallback: true`](/docs/api-reference/data-fetching/get-static-paths.md#fallback-true)
- [`getStaticPaths` with `fallback: true`](/docs/api-reference/data-fetching/get-static-paths.md#fallback-true)
- [`getStaticPaths` with `fallback: 'blocking'`](/docs/api-reference/data-fetching/get-static-paths.md#fallback-blocking)
- [`getServerSideProps`](/docs/basic-features/data-fetching/get-server-side-props.md)

### `getInitialProps`
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ export function getDefineEnv({
// pass domains in development to allow validating on the client
domains: config.images.domains,
remotePatterns: config.images?.remotePatterns,
output: config.output,
}
: {}),
}),
Expand Down
14 changes: 13 additions & 1 deletion packages/next/src/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ if (typeof window === 'undefined') {

const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const
type LoadingValue = typeof VALID_LOADING_VALUES[number]
type ImageConfig = ImageConfigComplete & { allSizes: number[] }
type ImageConfig = ImageConfigComplete & {
allSizes: number[]
output?: 'standalone' | 'export'
}

export type { ImageLoaderProps }
export type ImageLoader = (p: ImageLoaderProps) => string
Expand Down Expand Up @@ -645,6 +648,15 @@ const Image = forwardRef<HTMLImageElement | null, ImageProps>(
const qualityInt = getInt(quality)

if (process.env.NODE_ENV !== 'production') {
if (config.output === 'export' && isDefaultLoader && !unoptimized) {
throw new Error(
`Image Optimization using Next.js' default loader is not compatible with \`{ output: "export" }\`.
Possible solutions:
- Configure \`{ output: "standalone" }\` or remove it to run server mode including the Image Optimization API.
- Configure \`{ images: { unoptimized: true } }\` in \`next.config.js\` to disable the Image Optimization API.
Read more: https://nextjs.org/docs/messages/export-image-api`
)
}
if (!src) {
// React doesn't show the stack trace and there's
// no `src` to help identify which image, so we
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ export default async function exportApp(
runtime: nextConfig.experimental.runtime,
crossOrigin: nextConfig.crossOrigin,
optimizeCss: nextConfig.experimental.optimizeCss,
nextConfigOutput: nextConfig.output,
nextScriptWorkers: nextConfig.experimental.nextScriptWorkers,
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
fontManifest?: FontManifest
disableOptimizedLoading?: boolean
optimizeCss: any
nextConfigOutput: 'standalone' | 'export'
nextScriptWorkers: any
locale?: string
locales?: string[]
Expand Down Expand Up @@ -409,6 +410,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
? this.getFontManifest()
: undefined,
optimizeCss: this.nextConfig.experimental.optimizeCss,
nextConfigOutput: this.nextConfig.output,
nextScriptWorkers: this.nextConfig.experimental.nextScriptWorkers,
disableOptimizedLoading: this.nextConfig.experimental.runtime
? true
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ const configSchema = {
},
output: {
// automatic typing doesn't like enum
enum: ['standalone'] as any,
enum: ['standalone', 'export'] as any,
type: 'string',
},
outputFileTracing: {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ export interface NextConfig extends Record<string, any> {
}
}

output?: 'standalone'
output?: 'standalone' | 'export'

// A list of packages that should always be transpiled and bundled in the server
transpilePackages?: string[]
Expand Down
23 changes: 23 additions & 0 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,29 @@ function assignDefaults(

const result = { ...defaultConfig, ...config }

if (result.output === 'export') {
if (result.i18n) {
throw new Error(
'Specified "i18n" cannot but used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
if (result.rewrites) {
throw new Error(
'Specified "rewrites" cannot but used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
if (result.redirects) {
throw new Error(
'Specified "redirects" cannot but used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
if (result.headers) {
throw new Error(
'Specified "headers" cannot but used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
}

if (typeof result.assetPrefix !== 'string') {
throw new Error(
`Specified assetPrefix is not a string, found type "${typeof result.assetPrefix}" https://nextjs.org/docs/messages/invalid-assetprefix`
Expand Down
28 changes: 28 additions & 0 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,12 @@ export default class DevServer extends Server {
})

if (isMiddlewareFile(rootFile)) {
if (this.nextConfig.output === 'export') {
Log.error(
'Middleware cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
continue
}
this.actualMiddlewareFile = rootFile
middlewareMatchers = staticInfo.middleware?.matchers || [
{ regexp: '.*' },
Expand All @@ -513,6 +519,16 @@ export default class DevServer extends Server {
keepIndex: isAppPath,
})

if (
pageName.startsWith('/api/') &&
this.nextConfig.output === 'export'
) {
Log.error(
'API Routes cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
continue
}

if (isAppPath) {
if (!isLayoutsLeafPage(fileName, this.nextConfig.pageExtensions)) {
continue
Expand Down Expand Up @@ -1536,6 +1552,18 @@ export default class DevServer extends Server {
await withCoalescedInvoke(__getStaticPaths)(`staticPaths-${pathname}`, [])
).value

if (this.nextConfig.output === 'export') {
if (fallback === 'blocking') {
throw new Error(
'getStaticPaths with "fallback: blocking" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
} else if (fallback === true) {
throw new Error(
'getStaticPaths with "fallback: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
}

return {
staticPaths,
fallbackMode:
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export async function optimizeImage({
quality: number
width: number
height?: number
nextConfigOutput?: 'standalone'
nextConfigOutput?: 'standalone' | 'export'
}): Promise<Buffer> {
let optimizedBuffer = buffer
if (sharp) {
Expand Down Expand Up @@ -449,7 +449,7 @@ export async function optimizeImage({
optimizedBuffer = await transformer.toBuffer()
// End sharp transformation logic
} else {
if (showSharpMissingWarning && nextConfigOutput) {
if (showSharpMissingWarning && nextConfigOutput === 'standalone') {
// TODO: should we ensure squoosh also works even though we don't
// recommend it be used in production and this is a production feature
console.error(
Expand Down
10 changes: 9 additions & 1 deletion packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ export default class NextNodeServer extends BaseServer {
type: 'route',
name: '_next/image catchall',
fn: async (req, res, _params, parsedUrl) => {
if (this.minimalMode) {
if (this.minimalMode || this.nextConfig.output === 'export') {
res.statusCode = 400
res.body('Bad Request').send()
return {
Expand Down Expand Up @@ -1235,6 +1235,10 @@ export default class NextNodeServer extends BaseServer {
const edgeFunctionsPages = this.getEdgeFunctionsPages()
for (const edgeFunctionsPage of edgeFunctionsPages) {
if (edgeFunctionsPage === match.definition.page) {
if (this.nextConfig.output === 'export') {
await this.render404(req, res, parsedUrl)
return { finished: true }
}
delete query._nextBubbleNoFallback

const handledAsEdgeFunction = await this.runEdgeFunction({
Expand All @@ -1257,6 +1261,10 @@ export default class NextNodeServer extends BaseServer {
// it.
// TODO: move this behavior into a route handler.
if (match.definition.kind === RouteKind.PAGES_API) {
if (this.nextConfig.output === 'export') {
await this.render404(req, res, parsedUrl)
return { finished: true }
}
delete query._nextBubbleNoFallback

handled = await this.handleApiRequest(
Expand Down
13 changes: 13 additions & 0 deletions packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type RenderOptsPartial = {
optimizeFonts: FontConfig
fontManifest?: FontManifest
optimizeCss: any
nextConfigOutput?: 'standalone' | 'export'
nextScriptWorkers: any
devOnlyCacheBusterQueryString?: string
resolvedUrl?: string
Expand Down Expand Up @@ -467,6 +468,12 @@ export async function renderToHTML(
throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
}

if (getServerSideProps && renderOpts.nextConfigOutput === 'export') {
throw new Error(
'getServerSideProps cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}

if (getStaticPaths && !pageIsDynamic) {
throw new Error(
`getStaticPaths is only allowed for dynamic SSG pages and was found on '${pathname}'.` +
Expand Down Expand Up @@ -846,6 +853,11 @@ export async function renderToHTML(
}

if ('revalidate' in data) {
if (data.revalidate && renderOpts.nextConfigOutput === 'export') {
throw new Error(
'ISR cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
)
}
if (typeof data.revalidate === 'number') {
if (!Number.isInteger(data.revalidate)) {
throw new Error(
Expand Down Expand Up @@ -1400,6 +1412,7 @@ export async function renderToHTML(
crossOrigin: renderOpts.crossOrigin,
optimizeCss: renderOpts.optimizeCss,
optimizeFonts: renderOpts.optimizeFonts,
nextConfigOutput: renderOpts.nextConfigOutput,
nextScriptWorkers: renderOpts.nextScriptWorkers,
runtime: globalRuntime,
largePageDataBytes: renderOpts.largePageDataBytes,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/shared/lib/html-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type HtmlProps = {
crossOrigin?: string
optimizeCss?: any
optimizeFonts?: FontConfig
nextConfigOutput?: 'standalone' | 'export'
nextScriptWorkers?: boolean
runtime?: ServerRuntime
hasConcurrentFeatures?: boolean
Expand Down
4 changes: 4 additions & 0 deletions test/integration/config-output-export/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// prettier-ignore
module.exports = {
output: 'export',
}
1 change: 1 addition & 0 deletions test/integration/config-output-export/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'Hello World'

0 comments on commit 25efdfa

Please sign in to comment.