Skip to content
Merged
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
63 changes: 43 additions & 20 deletions nsm/scripts/copy-nextjs-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,56 @@ const path = require("path")
const prettier = require("prettier")
const fs = require("fs/promises")
const { existsSync } = require("fs")
const { pathToFileURL } = require("url")

async function copyNextjsConfig() {
const nextConfigPath = path.resolve(__dirname, "../../next.config.js")
// A module namespace is not the config, the config is its default export.
// The require below returns a namespace for an ES module in two cases: since
// Node.js 22.12 require of an ES module succeeds and returns one, and the nsm
// bin registers esbuild-runner, which transpiles an ES module to CommonJS and
// marks the result with __esModule. Reading the config off the namespace left
// rewrites undefined, which silently dropped it from the generated config.
const interopDefault = (value) => {
if (value == null) return value

const is_namespace =
value[Symbol.toStringTag] === "Module" || value.__esModule === true

return is_namespace && "default" in value ? value.default : value
}

let nextConfig = {}
if (existsSync(nextConfigPath)) {
async function loadNextjsConfig(nextConfigPath) {
let nextConfig
try {
nextConfig = interopDefault(require(nextConfigPath))
} catch (errorA) {
try {
nextConfig = require(nextConfigPath)
} catch (errorA) {
try {
nextConfig = (await import(nextConfigPath)).default
} catch (errorB) {
console.error(errorA)
console.error(errorB)
throw new Error(`Failed to load ${nextConfigPath}`)
}
nextConfig = interopDefault(await import(pathToFileURL(nextConfigPath)))
} catch (errorB) {
console.error(errorA)
console.error(errorB)
throw new Error(`Failed to load ${nextConfigPath}`)
}
}

if (typeof nextConfig === "function") {
nextConfig = await nextConfig()
}
if (typeof nextConfig === "function") {
nextConfig = await nextConfig()
}

if (typeof nextConfig.rewrites === "function") {
nextConfig.rewrites = await nextConfig.rewrites()
}
// A module namespace is frozen, so resolve rewrites into a copy.
if (typeof nextConfig.rewrites === "function") {
nextConfig = { ...nextConfig, rewrites: await nextConfig.rewrites() }
}

return nextConfig
}

async function copyNextjsConfig() {
const nextConfigPath = path.resolve(__dirname, "../../next.config.js")

const nextConfig = existsSync(nextConfigPath)
? await loadNextjsConfig(nextConfigPath)
: {}

const nextConfigFile = await prettier.format(
`export default ${JSON.stringify(nextConfig)}`,
{ semi: false, parser: "babel" },
Expand All @@ -40,7 +63,7 @@ async function copyNextjsConfig() {
)
}

module.exports = { copyNextjsConfig }
module.exports = { copyNextjsConfig, loadNextjsConfig }

if (require.main === module) {
copyNextjsConfig()
Expand Down
13 changes: 13 additions & 0 deletions tests/assets/next-configs/commonjs-function/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = async () => ({
reactStrictMode: true,
async rewrites() {
return {
beforeFiles: [
{
source: "/:path*",
destination: "/api/:path*",
},
],
}
},
})
13 changes: 13 additions & 0 deletions tests/assets/next-configs/commonjs/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
reactStrictMode: true,
async rewrites() {
return {
beforeFiles: [
{
source: "/:path*",
destination: "/api/:path*",
},
],
}
},
}
13 changes: 13 additions & 0 deletions tests/assets/next-configs/module/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
reactStrictMode: true,
async rewrites() {
return {
beforeFiles: [
{
source: "/:path*",
destination: "/api/:path*",
},
],
}
},
}
5 changes: 5 additions & 0 deletions tests/assets/next-configs/module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "next-config-module-fixture",
"private": true,
"type": "module"
}
40 changes: 40 additions & 0 deletions tests/load-nextjs-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test from "ava"
import { loadNextjsConfig } from "nsm/scripts/copy-nextjs-config"
import path from "path"

const configPath = (name: string) =>
path.resolve(__dirname, "assets", "next-configs", name, "next.config.js")

const expected_rewrites = {
beforeFiles: [
{
source: "/:path*",
destination: "/api/:path*",
},
],
}

test("loads a CommonJS config", async (t) => {
const config = await loadNextjsConfig(configPath("commonjs"))

t.true(config.reactStrictMode)
t.deepEqual(config.rewrites, expected_rewrites)
})

test("loads a CommonJS config exporting a function", async (t) => {
const config = await loadNextjsConfig(configPath("commonjs-function"))

t.true(config.reactStrictMode)
t.deepEqual(config.rewrites, expected_rewrites)
})

// The nsm bin registers esbuild-runner, which transpiles an ES module to
// CommonJS, and require of an ES module returns a module namespace since
// Node.js 22.12. Both give the require in loadNextjsConfig a namespace instead
// of the config, and reading rewrites off the namespace silently dropped it.
test("loads an ES module config", async (t) => {
const config = await loadNextjsConfig(configPath("module"))

t.true(config.reactStrictMode)
t.deepEqual(config.rewrites, expected_rewrites)
})
Loading