Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for output: export config #46744

Merged
merged 24 commits into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -301,6 +301,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 @@ -701,7 +701,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 @@ -543,7 +543,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 @@ -1534,6 +1550,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'