From c74e4f21ff00c5c9fbb38fbfa1c068acaf5d189d Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 7 Feb 2022 09:48:35 +0100 Subject: [PATCH] Add support for async fn / promise in next.config.js/.mjs (#33662) - 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` --- .../next.config.js/introduction.md | 14 ++++++++ errors/promise-in-next-config.md | 4 ++- packages/next/server/config-shared.ts | 11 ++----- packages/next/server/config.ts | 2 +- .../async-function.test.ts | 33 +++++++++++++++++++ .../e2e/config-promise-export/promise.test.ts | 33 +++++++++++++++++++ .../config-promise-error/test/index.test.js | 22 ------------- 7 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 test/e2e/config-promise-export/async-function.test.ts create mode 100644 test/e2e/config-promise-export/promise.test.ts diff --git a/docs/api-reference/next.config.js/introduction.md b/docs/api-reference/next.config.js/introduction.md index 6ecd28297c89c..121510630502d 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 f39ce7786fc78..84ac204fa85c9 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 177ddb92a2c62..8c6270666a26c 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 fd91abc117e1e..04558b3656fbb 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 0000000000000..0869155f1c517 --- /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 0000000000000..5b7889cc9462d --- /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 4547b92cbe1d1..b5f4a20b98228 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'),