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

Load next.config.mjs as ESM #22153

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const eslint = new ESLint()
const isWin = process.platform === 'win32'

module.exports = {
'**/*.{js,jsx,ts,tsx}': (filenames) => {
'**/*.{js,jsx,ts,tsx,mjs}': (filenames) => {
const escapedFileNames = filenames
.map((filename) => `"${isWin ? filename : escape([filename])}"`)
.join(' ')
Expand Down
6 changes: 6 additions & 0 deletions packages/next/lib/native-dynamic-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// The 'babel-plugin-dynamic-import-node' babel plugin will be disabled for
// this file so that we can use it as an escape hatch to the native import()
// implementation
export default async function nativeImport(mod: string) {
return import(mod)
}
23 changes: 21 additions & 2 deletions packages/next/next-server/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { execOnce } from '../lib/utils'
import { defaultConfig, normalizeConfig } from './config-shared'
import { loadWebpackHook } from './config-utils'
import { ImageConfig, imageConfigDefault, VALID_LOADERS } from './image-config'
import nativeImport from '../../lib/native-dynamic-import'
import { loadEnvConfig } from '@next/env'

export { DomainLocales, NextConfig, normalizeConfig } from './config-shared'
Expand Down Expand Up @@ -406,6 +407,23 @@ function assignDefaults(userConfig: { [key: string]: any }) {
return result
}

async function findConfigFile(dir: string): Promise<string | undefined> {
const files = [CONFIG_FILE.replace(/\.js$/, '.mjs'), CONFIG_FILE]
return findUp(files, { cwd: dir })
}

async function importCjsOrEsm(mod: string): Promise<any> {
try {
return require(mod)
} catch (err) {
if (err.code === 'ERR_REQUIRE_ESM') {
const { default: defaultExport } = await nativeImport(mod)
return defaultExport
}
throw err
}
}

export default async function loadConfig(
phase: string,
dir: string,
Expand All @@ -418,11 +436,12 @@ export default async function loadConfig(
return assignDefaults({ configOrigin: 'server', ...customConfig })
}

const path = await findUp(CONFIG_FILE, { cwd: dir })
const path = await findConfigFile(dir)

// If config file was found
if (path?.length) {
const userConfigModule = require(path)
const userConfigModule = await importCjsOrEsm(path)

const userConfig = normalizeConfig(
phase,
userConfigModule.default || userConfigModule
Expand Down
12 changes: 8 additions & 4 deletions packages/next/taskfile-babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,22 @@ const babelServerOpts = {
loose: true,
// This is handled by the Next.js webpack config that will run next/babel over the same code.
exclude: [
// dynamic import() is handled by 'babel-plugin-dynamic-import-node'
'proposal-dynamic-import',
'transform-typeof-symbol',
'transform-async-to-generator',
'transform-spread',
],
},
],
],
plugins: [
'babel-plugin-dynamic-import-node',
['@babel/plugin-proposal-class-properties', { loose: true }],
],
plugins: [['@babel/plugin-proposal-class-properties', { loose: true }]],
overrides: [
{
plugins: ['babel-plugin-dynamic-import-node'],
// This is our escape hatch to the native node.js dynamic import:
exclude: /\/lib\/native-dynamic-import\.ts$/,
},
{
test: /\.tsx?$/,
// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down
5 changes: 5 additions & 0 deletions test/integration/config-esm/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
env: {
customVar: 'hello',
},
}
3 changes: 3 additions & 0 deletions test/integration/config-esm/pages/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div id="custom-var">{process.env.customVar}</div>
}
62 changes: 62 additions & 0 deletions test/integration/config-esm/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-env jest */

import webdriver from 'next-webdriver'

import {
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
} from 'next-test-utils'
import { join } from 'path'

jest.setTimeout(1000 * 60 * 2)

let app
let appPort
const appDir = join(__dirname, '../')

function runTests() {
it('should have loaded next.config.mjs', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
await browser.waitForElementByCss('#custom-var')
const text = await browser.elementByCss('#custom-var').text()
expect(text).toBe('hello')
} finally {
if (browser) await browser.close()
}
})
}

const nodeVersion = Number(process.versions.node.split('.')[0])
const skipTests = nodeVersion < 12

;(skipTests ? describe.skip : describe)('next.config.mjs', () => {
describe('dev mode', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})

runTests()
})

describe('production mode', () => {
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})

runTests()
})
})