Skip to content

Commit

Permalink
feat: change next build to emit output with output: export (#47376)
Browse files Browse the repository at this point in the history
This PR ensures the correct output is emitted during `next build` and
deprecates `next export`.

The `output: export` configuration tells it to emit exported html and
the `distDir: out` configures the output directory.

```js
module.exports = {
  output: 'export',
  distDir: 'out',
}
```

fix NEXT-868 ([link](https://linear.app/vercel/issue/NEXT-868))

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
styfle and kodiakhq[bot] committed Mar 23, 2023
1 parent 394bff5 commit 9791d1e
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 69 deletions.
64 changes: 54 additions & 10 deletions docs/advanced-features/static-html-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ The core of Next.js has been designed to enable starting as a static site (or Si

Since Next.js supports this static export, it can be deployed and hosted on any web server that can serve HTML/CSS/JS static assets.

## `next export`
## Usage

Update your `next.config.js` file to include `output: "export"` like the following:
Update your [`next.config.js`](/docs/api-reference/next.config.js/introduction.md) file to include `output: 'export'` like the following:

```js
/**
Expand All @@ -32,21 +32,45 @@ const nextConfig = {
module.exports = nextConfig
```

Update your scripts in `package.json` file to include `next export` like the following:
Then run `next build` to generate an `out` directory containing the HTML/CSS/JS static assets.

```json
"scripts": {
"build": "next build && next export"
You can utilize [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) to generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md)).

If you want to change the output directory, you can configure `distDir` like the following:

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

module.exports = nextConfig
```

Running `npm run build` will generate an `out` directory.
In this example, `next build` will generate a `dist` directory containing the HTML/CSS/JS static assets.

`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md)). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`.
Learn more about [Setting a custom build directory](/docs/api-reference/next.config.js/setting-a-custom-build-directory.md).

If you want to change the output directory structure to always include a trailing slash, you can configure `trailingSlash` like the following:

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

module.exports = nextConfig
```

For more advanced scenarios, you can define a parameter called [`exportPathMap`](/docs/api-reference/next.config.js/exportPathMap.md) in your [`next.config.js`](/docs/api-reference/next.config.js/introduction.md) file to configure exactly which pages will be generated.
This will change links so that `href="/about"` will instead be `herf="/about/"`. It will also change the output so that `out/about.html` will instead emit `out/about/index.html`.

> **Warning**: Using `exportPathMap` is deprecated and is overridden by `getStaticPaths` inside `pages`. We recommend not to use them together.
Learn more about [Trailing Slash](/docs/api-reference/next.config.js/trailing-slash.md).

## Supported Features

Expand Down Expand Up @@ -88,3 +112,23 @@ It's possible to use the [`getInitialProps`](/docs/api-reference/data-fetching/g
- `getInitialProps` should fetch from an API and cannot use Node.js-specific libraries or the file system like `getStaticProps` can.

We recommend migrating towards `getStaticProps` over `getInitialProps` whenever possible.

## next export

> **Warning**: "next export" is deprecated since Next.js 13.3 in favor of "output: 'export'" configuration.
In versions of Next.js prior to 13.3, there was no configuration option in next.config.js and instead there was a separate command for `next export`.

This could be used by updating your `package.json` file to include `next export` like the following:

```json
"scripts": {
"build": "next build && next export"
}
```

Running `npm run build` will generate an `out` directory.

`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md)). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`.

> **Warning**: Using [`exportPathMap`](/docs/api-reference/next.config.js/exportPathMap.md) is deprecated and is overridden by `getStaticPaths` inside `pages`. We recommend not to use them together.
65 changes: 46 additions & 19 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,16 @@ export default async function build(
.traceAsyncFn(() => loadConfig(PHASE_PRODUCTION_BUILD, dir))
NextBuildContext.config = config

let configOutDir = 'out'
if (config.output === 'export' && config.distDir !== '.next') {
// In the past, a user had to run "next build" to generate
// ".next" (or whatever the distDir) followed by "next export"
// to generate "out" (or whatever the outDir). However, when
// "output: export" is configured, "next build" does both steps.
// So the user-configured dirDir is actually the outDir.
configOutDir = config.distDir
config.distDir = '.next'
}
const distDir = path.join(dir, config.distDir)
setGlobal('phase', PHASE_PRODUCTION_BUILD)
setGlobal('distDir', distDir)
Expand Down Expand Up @@ -2314,24 +2324,7 @@ export default async function build(
)
const exportApp: typeof import('../export').default =
require('../export').default
const exportOptions: ExportOptions = {
silent: false,
buildExport: true,
debugOutput,
threads: config.experimental.cpus,
pages: combinedPages,
outdir: path.join(distDir, 'export'),
statusMessage: 'Generating static pages',
exportPageWorker: sharedPool
? staticWorkers.exportPage.bind(staticWorkers)
: undefined,
endWorker: sharedPool
? async () => {
await staticWorkers.end()
}
: undefined,
appPaths,
}

const exportConfig: NextConfigComplete = {
...config,
initialPageRevalidationMap: {},
Expand Down Expand Up @@ -2451,7 +2444,28 @@ export default async function build(
},
}

await exportApp(dir, exportOptions, nextBuildSpan, exportConfig)
const exportOptions: ExportOptions = {
isInvokedFromCli: false,
nextConfig: exportConfig,
silent: false,
buildExport: true,
debugOutput,
threads: config.experimental.cpus,
pages: combinedPages,
outdir: path.join(distDir, 'export'),
statusMessage: 'Generating static pages',
exportPageWorker: sharedPool
? staticWorkers.exportPage.bind(staticWorkers)
: undefined,
endWorker: sharedPool
? async () => {
await staticWorkers.end()
}
: undefined,
appPaths,
}

await exportApp(dir, exportOptions, nextBuildSpan)

const postBuildSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Finalizing page optimization`,
Expand Down Expand Up @@ -3099,6 +3113,19 @@ export default async function build(
})
}

if (config.output === 'export') {
const exportApp: typeof import('../export').default =
require('../export').default
const options: ExportOptions = {
isInvokedFromCli: false,
nextConfig: config,
silent: true,
threads: config.experimental.cpus,
outdir: path.join(dir, configOutDir),
}
await exportApp(dir, options, nextBuildSpan)
}

await nextBuildSpan
.traceChild('telemetry-flush')
.traceAsyncFn(() => telemetry.flush())
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/cli/next-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const nextExport: CliCommand = (argv) => {
silent: args['--silent'] || false,
threads: args['--threads'],
outdir: args['--outdir'] ? resolve(args['--outdir']) : join(dir, 'out'),
isInvokedFromCli: true,
}

exportApp(dir, options, nextExportCliSpan)
Expand Down
36 changes: 25 additions & 11 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const createProgress = (total: number, label: string) => {

export interface ExportOptions {
outdir: string
isInvokedFromCli: boolean
silent?: boolean
threads?: number
debugOutput?: boolean
Expand All @@ -154,13 +155,13 @@ export interface ExportOptions {
exportPageWorker?: typeof import('./worker').default
endWorker?: () => Promise<void>
appPaths?: string[]
nextConfig?: NextConfigComplete
}

export default async function exportApp(
dir: string,
options: ExportOptions,
span: Span,
configuration?: NextConfigComplete
span: Span
): Promise<void> {
const nextExportSpan = span.traceChild('next-export')
const hasAppDir = !!options.appPaths
Expand All @@ -174,10 +175,24 @@ export default async function exportApp(
.traceFn(() => loadEnvConfig(dir, false, Log))

const nextConfig =
configuration ||
options.nextConfig ||
(await nextExportSpan
.traceChild('load-next-config')
.traceAsyncFn(() => loadConfig(PHASE_EXPORT, dir)))

if (options.isInvokedFromCli) {
if (nextConfig.output === 'export') {
Log.warn(
'"next export" is no longer needed when "output: export" is configured in next.config.js'
)
return
} else {
Log.warn(
'"next export" is deprecated in favor of "output: export" in next.config.js https://nextjs.org/docs/advanced-features/static-html-export'
)
}
}

const threads = options.threads || nextConfig.experimental.cpus
const distDir = join(dir, nextConfig.distDir)

Expand Down Expand Up @@ -627,7 +642,7 @@ export default async function exportApp(
)
}

const timeout = configuration?.staticPageGenerationTimeout || 0
const timeout = nextConfig?.staticPageGenerationTimeout || 0
let infoPrinted = false
let exportPage: typeof import('./worker').default
let endWorker: () => Promise<void>
Expand Down Expand Up @@ -714,23 +729,22 @@ export default async function exportApp(
errorPaths.push(page !== path ? `${page}: ${path}` : path)
}

if (options.buildExport && configuration) {
if (options.buildExport) {
if (typeof result.fromBuildExportRevalidate !== 'undefined') {
configuration.initialPageRevalidationMap[path] =
nextConfig.initialPageRevalidationMap[path] =
result.fromBuildExportRevalidate
}

if (typeof result.fromBuildExportMeta !== 'undefined') {
configuration.initialPageMetaMap[path] =
result.fromBuildExportMeta
nextConfig.initialPageMetaMap[path] = result.fromBuildExportMeta
}

if (result.ssgNotFound === true) {
configuration.ssgNotFoundPaths.push(path)
nextConfig.ssgNotFoundPaths.push(path)
}

const durations = (configuration.pageDurationMap[pathMap.page] =
configuration.pageDurationMap[pathMap.page] || {})
const durations = (nextConfig.pageDurationMap[pathMap.page] =
nextConfig.pageDurationMap[pathMap.page] || {})
durations[path] = result.duration
}

Expand Down

0 comments on commit 9791d1e

Please sign in to comment.