Skip to content

Commit 23ca5eb

Browse files
committed
fix(next-mdx): resolve ESM-only plugins passed as strings
When an MDX plugin is passed as a string (e.g. `rehypePlugins: ['@stefanprobst/rehype-extract-toc']`), the loader resolves it via `require.resolve()` which uses CJS resolution semantics. Packages that ship with an `exports` field containing only an `"import"` condition (no `"require"` condition) cause `require.resolve` to throw ERR_PACKAGE_PATH_NOT_EXPORTED because no CJS-compatible export entry is found. Catch that specific error and fall back to a direct ESM dynamic `import()`, which uses the ESM resolver and correctly matches the `"import"` condition in the exports map. Fixes #73757
1 parent 8e5a36f commit 23ca5eb

File tree

1 file changed

+58
-2
lines changed

1 file changed

+58
-2
lines changed

packages/next-mdx/mdx-js-loader.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,73 @@
11
const mdxLoader = require('@mdx-js/loader')
2+
const fs = require('node:fs')
3+
const path = require('node:path')
24
const { pathToFileURL } = require('node:url')
35

46
function interopDefault(mod) {
57
return mod.default || mod
68
}
79

10+
// Recursively resolve an exports condition value, preferring 'import' then 'default'.
11+
function resolveExportCondition(value) {
12+
if (typeof value === 'string') return value
13+
if (value && typeof value === 'object') {
14+
for (const key of ['import', 'default']) {
15+
if (key in value) {
16+
const result = resolveExportCondition(value[key])
17+
if (result) return result
18+
}
19+
}
20+
}
21+
return null
22+
}
23+
24+
// Extract the ESM entry point from a package.json object.
25+
function resolveEsmEntry(pkg) {
26+
const { exports: exp, module: mod, main } = pkg
27+
if (exp) {
28+
const mainExport = typeof exp === 'string' ? exp : exp['.'] ?? exp
29+
const resolved = resolveExportCondition(mainExport)
30+
if (resolved) return resolved
31+
}
32+
return mod ?? main ?? 'index.js'
33+
}
34+
835
async function importPluginForPath(pluginPath, projectRoot) {
9-
const path = require.resolve(pluginPath, { paths: [projectRoot] })
36+
let resolvedPath
37+
try {
38+
resolvedPath = require.resolve(pluginPath, { paths: [projectRoot] })
39+
} catch (e) {
40+
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
41+
// The package defines an `exports` field but has no entry for the
42+
// `"require"` condition (e.g. an ESM-only package whose exports only
43+
// include an `"import"` condition). `require.resolve` uses CJS
44+
// resolution semantics and cannot match those entries.
45+
//
46+
// Node.js always includes the absolute path to the package's package.json
47+
// in the error message. That path was resolved starting from `projectRoot`,
48+
// so we can use it to find the ESM entry point and import via a file:// URL.
49+
// A bare import(pluginPath) would resolve from @next/mdx's own location,
50+
// which fails in pnpm / non-hoisted setups.
51+
const pkgJsonMatch = e.message.match(/in (.+package\.json)/)
52+
if (pkgJsonMatch) {
53+
const pkgJsonPath = pkgJsonMatch[1]
54+
const pkgDir = path.dirname(pkgJsonPath)
55+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
56+
const entry = resolveEsmEntry(pkg)
57+
const absolutePath = path.resolve(pkgDir, entry)
58+
return interopDefault(await import(pathToFileURL(absolutePath)))
59+
}
60+
// Could not extract the path from the error message — fall back to a bare
61+
// import (may fail in strict pnpm / non-hoisted setups).
62+
return interopDefault(await import(pluginPath))
63+
}
64+
throw e
65+
}
1066
return interopDefault(
1167
// "use pathToFileUrl to make esm import()s work with absolute windows paths":
1268
// on windows import("C:\\path\\to\\file") is not valid, so we need to use file:// URLs
1369
// https://github.com/vercel/next.js/commit/fbf9e12de095e0237d4ba4aa6139d9757bd20be9
14-
await import(process.platform === 'win32' ? pathToFileURL(path) : path)
70+
await import(process.platform === 'win32' ? pathToFileURL(resolvedPath) : resolvedPath)
1571
)
1672
}
1773

0 commit comments

Comments
 (0)