diff --git a/docs/api-reference/next.config.js/introduction.md b/docs/api-reference/next.config.js/introduction.md index 6ecd28297c89ce5..121510630502d40 100644 --- a/docs/api-reference/next.config.js/introduction.md +++ b/docs/api-reference/next.config.js/introduction.md @@ -48,6 +48,20 @@ module.exports = (phase, { defaultConfig }) => { } ``` +Since Next.js 12.0.10, you can use an async function: + +```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 diff --git a/errors/promise-in-next-config.md b/errors/promise-in-next-config.md index f39ce7786fc7896..84ac204fa85c9e9 100644 --- a/errors/promise-in-next-config.md +++ b/errors/promise-in-next-config.md @@ -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. diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index 177ddb92a2c6280..8c6270666a26c99 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -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 } diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index fd91abc117e1ea1..04558b3656fbb37 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -585,7 +585,7 @@ export default async function loadConfig( ) throw err } - const userConfig = normalizeConfig( + const userConfig = await normalizeConfig( phase, userConfigModule.default || userConfigModule ) diff --git a/test/e2e/config-promise-export/async-function.test.ts b/test/e2e/config-promise-export/async-function.test.ts new file mode 100644 index 000000000000000..0869155f1c517ed --- /dev/null +++ b/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

hello world

+ } + `, + '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') + }) +}) diff --git a/test/e2e/config-promise-export/promise.test.ts b/test/e2e/config-promise-export/promise.test.ts new file mode 100644 index 000000000000000..5b7889cc9462da6 --- /dev/null +++ b/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

hello world

+ } + `, + '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') + }) +}) diff --git a/test/integration/config-promise-error/test/index.test.js b/test/integration/config-promise-error/test/index.test.js index 4547b92cbe1d1e2..b5f4a20b982282b 100644 --- a/test/integration/config-promise-error/test/index.test.js +++ b/test/integration/config-promise-error/test/index.test.js @@ -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'),