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

feat: change next build to emit output with output: export #47376

Merged
merged 15 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might have been a bug before.

Previously, it only read nextConfig if it was passed in as an arg, but not the case when it read the file on line 181 above

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