Skip to content

Commit

Permalink
Add support for async fn / promise in next.config.js/.mjs (#33662)
Browse files Browse the repository at this point in the history
- Add support for async function / promise export in next.config.js/.mjs
- Update docs

Adds support for https://twitter.com/timneutkens/status/1486075973204422665

But also the simpler version:

```js
module.exports = async () => {
  return {
    basePath: '/docs'
  }
}
```



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
  • Loading branch information
timneutkens committed Feb 7, 2022
1 parent 938eb0e commit c74e4f2
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 32 deletions.
14 changes: 14 additions & 0 deletions docs/api-reference/next.config.js/introduction.md
Expand Up @@ -48,6 +48,20 @@ module.exports = (phase, { defaultConfig }) => {
}
```

Since Next.js 12.0.10, you can use an async function:

This comment has been minimized.

Copy link
@julen

julen Feb 10, 2022

Unless I'm misreading the git tag, won't this be available only as of 12.0.11?


```js
module.exports = async (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* config options here */
}
return nextConfig
}
```

`phase` is the current context in which the configuration is loaded. You can see the [available phases](https://github.com/vercel/next.js/blob/canary/packages/next/shared/lib/constants.ts#L1-L5). Phases can be imported from `next/constants`:

```js
Expand Down
4 changes: 3 additions & 1 deletion errors/promise-in-next-config.md
Expand Up @@ -14,6 +14,8 @@ module.exports = {

#### Possible Ways to Fix It

Check your `next.config.js` for `async` or `return Promise`
In Next.js versions above `12.0.10`, `module.exports = async () =>` is supported.

For older versions, you can check your `next.config.js` for `async` or `return Promise`.

Potentially a plugin is returning a `Promise` from the webpack function.
11 changes: 3 additions & 8 deletions packages/next/server/config-shared.ts
Expand Up @@ -466,15 +466,10 @@ export const defaultConfig: NextConfig = {
},
}

export function normalizeConfig(phase: string, config: any) {
export async function normalizeConfig(phase: string, config: any) {
if (typeof config === 'function') {
config = config(phase, { defaultConfig })

if (typeof config.then === 'function') {
throw new Error(
'> Promise returned in next config. https://nextjs.org/docs/messages/promise-in-next-config'
)
}
}
return config
// Support `new Promise` and `async () =>` as return values of the config export
return await config
}
2 changes: 1 addition & 1 deletion packages/next/server/config.ts
Expand Up @@ -585,7 +585,7 @@ export default async function loadConfig(
)
throw err
}
const userConfig = normalizeConfig(
const userConfig = await normalizeConfig(
phase,
userConfigModule.default || userConfigModule
)
Expand Down
33 changes: 33 additions & 0 deletions test/e2e/config-promise-export/async-function.test.ts
@@ -0,0 +1,33 @@
import { createNext } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP } from 'next-test-utils'

describe('async export', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
'pages/index.js': `
export default function Page() {
return <p>hello world</p>
}
`,
'next.config.js': `
module.exports = async () => {
return {
basePath: '/docs'
}
}
`,
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should work', async () => {
const html = await renderViaHTTP(next.url, '/docs')
expect(html).toContain('hello world')
})
})
33 changes: 33 additions & 0 deletions test/e2e/config-promise-export/promise.test.ts
@@ -0,0 +1,33 @@
import { createNext } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP } from 'next-test-utils'

describe('promise export', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
'pages/index.js': `
export default function Page() {
return <p>hello world</p>
}
`,
'next.config.js': `
module.exports = new Promise((resolve) => {
resolve({
basePath: '/docs'
})
})
`,
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should work', async () => {
const html = await renderViaHTTP(next.url, '/docs')
expect(html).toContain('hello world')
})
})
22 changes: 0 additions & 22 deletions test/integration/config-promise-error/test/index.test.js
Expand Up @@ -9,28 +9,6 @@ const appDir = join(__dirname, '..')
describe('Promise in next config', () => {
afterEach(() => fs.remove(join(appDir, 'next.config.js')))

it('should throw error when a promise is return on config', async () => {
fs.writeFile(
join(appDir, 'next.config.js'),
`
module.exports = (phase, { isServer }) => {
return new Promise((resolve) => {
resolve({ target: 'serverless' })
})
}
`
)

const { stderr, stdout } = await nextBuild(appDir, undefined, {
stderr: true,
stdout: true,
})

expect(stderr + stdout).toMatch(
/Error: > Promise returned in next config\. https:\/\//
)
})

it('should warn when a promise is returned on webpack', async () => {
fs.writeFile(
join(appDir, 'next.config.js'),
Expand Down

0 comments on commit c74e4f2

Please sign in to comment.