Skip to content

MDX Plugin String Format resolution doesn't work with ESM plugins with multiple exports #73757

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

Open
wesbos opened this issue Dec 10, 2024 · 9 comments
Labels
bug Issue was opened via the bug report template. Markdown (MDX) Related to Markdown with Next.js.

Comments

@wesbos
Copy link
Contributor

wesbos commented Dec 10, 2024

Link to the code that reproduces this issue

https://github.com/wesbos/next-mdx-plugin-string-issue

To Reproduce

When using strings to import a MDX Rehype plugin, the Next.js importer fails.

I believe this is only when the package has multiple ESM exports - like this package: https://github.com/stefanprobst/rehype-extract-toc/blob/main/package.json

Thansk to @karlhorky for linking me to the possible commit / code. CC @timneutkens

https://github.com/vercel/next.js/pull/72802/files#diff-d5904dff78d88856dc003d263e6f70f0a607166230fba8e4b947a3bae5e4e87cR10

import createMDX from "@next/mdx";

const withMDX = createMDX({
  options: {
    rehypePlugins: [
      ["@stefanprobst/rehype-extract-toc"],
      ["@stefanprobst/rehype-extract-toc/mdx"],
    ],
  },
});

We get the Error:

Error: No "exports" main defined in /Users/wesbos/Sites/delete-me/mdx-plugin-issue/node_modules/@stefanprobst/rehype-extract-toc/package.json
    at Array.map (<anonymous>) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
 ⨯ unhandledRejection: Error: No "exports" main defined in /Users/wesbos/Sites/delete-me/mdx-plugin-issue/node_modules/@stefanprobst/rehype-extract-toc/package.json
    at Array.map (<anonymous>) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
 ⨯ unhandledRejection:  Error: No "exports" main defined in /Users/wesbos/Sites/delete-me/mdx-plugin-issue/node_modules/@stefanprobst/rehype-extract-toc/package.json
    at Array.map (<anonymous>) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Or

Error: Package subpath './mdx' is not defined by "exports" in /Users/wesbos/Sites/delete-me/mdx-plugin-issue/node_modules/@stefanprobst/rehype-extract-toc/package.json
    at Array.map (<anonymous>) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

This error does not happen if the plugin is imported inside next.config.mjs and passed as a javascript function, but since Turborepo must pass as a string, I cannot do this.

The error exists both with and without --turbo.

Current vs. Expected behavior

_

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.1.0: Thu Oct 10 21:03:15 PDT 2024; root:xnu-11215.41.3~2/RELEASE_ARM64_T6000
  Available memory (MB): 65536
  Available CPU cores: 10
Binaries:
  Node: 23.1.0
  npm: 10.9.0
  Yarn: 1.22.22
  pnpm: 9.10.0
Relevant Packages:
  next: 15.0.4-canary.51 // Latest available version is detected (15.0.4-canary.51).
  eslint-config-next: N/A
  react: 19.0.0-beta-04b058868c-20240508
  react-dom: 19.0.0-beta-04b058868c-20240508
  typescript: 5.1.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Markdown (MDX)

Which stage(s) are affected? (Select all that apply)

next dev (local)

Additional context

No response

@wesbos wesbos added the bug Issue was opened via the bug report template. label Dec 10, 2024
@github-actions github-actions bot added the Markdown (MDX) Related to Markdown with Next.js. label Dec 10, 2024
stefanprobst added a commit to stefanprobst/rehype-extract-toc that referenced this issue Dec 11, 2024
Seems some bundlers (webpack, turbopack) can't import this when the
rehype package is passed as a string.

Tracking the issue here, but adding the default key also fixes it

vercel/next.js#73757

Would appreciate if you could merge + cut a release.
@wesbos
Copy link
Contributor Author

wesbos commented Dec 13, 2024

It looks like the issue here is when the package doesn't have a "default" export - see the above PR that fixed it for me. I still think this is a bug though because I could import the package without issue, it only happened when passing a string

@max-degterev
Copy link

I am getting

TypeError: Expected usable value, not `remark-breaks`
    at add (file:///Volumes/Data/Projects/bldry-website/node_modules/unified/lib/index.js:1100:15)
    at addList (file:///Volumes/Data/Projects/bldry-website/node_modules/unified/lib/index.js:1134:11)
    at Function.use (file:///Volumes/Data/Projects/bldry-website/node_modules/unified/lib/index.js:1074:9)
    at createProcessor (file:///Volumes/Data/Projects/bldry-website/node_modules/@mdx-js/mdx/lib/core.js:209:6)
    at split (file:///Volumes/Data/Projects/bldry-website/node_modules/@mdx-js/mdx/lib/util/create-format-aware-processors.js:85:37)
    at process (file:///Volumes/Data/Projects/bldry-website/node_modules/@mdx-js/mdx/lib/util/create-format-aware-processors.js:62:31)
    at Proxy.loader (file:///Volumes/Data/Projects/bldry-website/node_modules/@mdx-js/loader/lib/index.js:89:3)
    at /Volumes/Data/Projects/bldry-website/node_modules/@mdx-js/loader/index.cjs:32:26
 ⨯ unhandledRejection: [TypeError: Expected usable value, not `remark-breaks`]
 ⨯ unhandledRejection:  [TypeError: Expected usable value, not `remark-breaks`]
[TypeError: Expected usable value, not `remark-breaks`]

with

const withMDX = createMDX({
  // Add markdown plugins here, as desired
  options: {
    remarkPlugins: ['remark-breaks'],
    rehypePlugins: [['rehype-external-links', {
      target: '_blank', rel: ['noopener', 'noreferrer', 'nofollow'],
    }]],
  },
});

Along with TS errors. Did you figure it out?

Screenshot 2024-12-30 at 3 53 48 PM

@chrisweb
Copy link
Contributor

chrisweb commented Jan 1, 2025

When using the string format, make sure every entry in the plugin list is an array (not only when you have options)

For example, instead of this (⚠️ following code does NOT work):

const withMDX = createMDX({
    options: {
        remarkPlugins: [],
        rehypePlugins: ['rehype-slug'],
    },
})

you need to put 'rehype-slug' (the plugin string name) into an array:

const withMDX = createMDX({
    options: {
        remarkPlugins: [],
        rehypePlugins: [['rehype-slug']],
    },
})

meaning that now the plugin entry is an array (that contains the string) and the list of plugins is an array too

Note

the new loader which adds support for MDX (remark / rehype) plugins (when using tubopack) made its first appearence in @next/mdx 15.1, so if your next.js version is older you need to import plugins instead (or upgrade to @next/mdx 15.1)

Screenshot 2024-12-30 at 3 53 48 PM

This typescript types error, @next/mdx is still using the types from the @mdxjs/loader (instead of defining new types in the @next/mdx/js-loader

To get rid of this error until the types are fixed, just use a typescript comment to let typescript know it should ignore the problem:

const withMDX = createMDX({
    options: {
        // @ts-ignore wrong types
        remarkPlugins: [['remark-gfm', remarkGFMOptions]],
        rehypePlugins: [],
    },
})

P.S. I have more info regarding mdx (rehype / remark) plugins in combination with turbopack and / or the mdx rust compiler in my next.js 15 mdx tutorial

@max-degterev
Copy link

It doesn't work when it's in an array form either. I've tried.

@wesbos
Copy link
Contributor Author

wesbos commented Jan 8, 2025

Hitting this again with the rehype-extract-excerpt package. Adding a default export to the package via a patch fixes is.

So the MDX resolution code still has a bug

Screenshot 2025-01-08 at 2 46 32 PM

@souporserious
Copy link
Contributor

I don't think this is exclusive to MDX resolution. Renaming my next config from mjs to ts revealed this error in similar packages not defining package exports correctly.

@wesbos
Copy link
Contributor Author

wesbos commented Feb 7, 2025

A couple more example issues referenced above 👆🏻

@Onlylonger
Copy link

Onlylonger commented Mar 22, 2025

It doesn't work when it's in an array form either. I've tried.

Have a try with

  experimental: {
    mdxRs: true,
  }

@HuxleyIsMe
Copy link

HuxleyIsMe commented May 19, 2025

Hello 👋

I went on a deep dive with this one in a quest to use emojis from remark-emojis. I ended up just writing a script to do it for me. Would appreciate knowing if I've missed something or followed the wrong thread. 🙂

Having a look, I found three points of failure — one is the known issue of not serializing to a string.

TLDR:

Next.js cannot work with ES6 packages that contain only 'import' pathways in their 'exports' field of their package.json. This is due to its use of 'require' in two core places.

Firstly, what do I mean by import of their exports pathway?

Node introduced multiple entry points to modules within the package.json. One reason was to ease the bridge between ES6 modules and CommonJS. We could now specify an entry point to the CommonJS modules as well as to the ES6 modules in one package. To do this, we use the 'exports' field in our package.json to specify which way 'import' should go, which way 'require' should go, and other functionalities or default settings.

Node.js Package Entry Points

More about Exports

With packages like remark-emoji, the exports field only specifies an import pathway:

"exports": {
".": {
"import": {
"types": "./index.d.ts",
"default": "./index.js"
}
}
},

If there was a default one level higher, then require would go here. Since there isn't, there is no valid entry point for require, which is correct as this package exclusively uses ES6 modules.

@wesbos, this is why you see the addition of default in your example working — because require is flowing that way.

Ok, but why isn't it happening in Next.js? Next.js seems to support ES6.

Bundling Issue with next.config.ts:

When bundling using next.config.ts (note the .ts), we encounter:

ERR_PACKAGE_PATH_NOT_EXPORTED

To replicate, simply write:

import emoji from 'remark-emoji';

This error message comes from the resolve-pkg-maps package. This package is a dependency of 'get-tsconfig', which Next.js uses during the configuration process. The error itself arises because the package being imported does not provide a valid entry point for require, which results in the error. To be clear, resolve-pkg-maps works as expected when given the correct conditionals, but somewhere along the flow it is not. The package remark-emoji is also correct, its exports key is valid. Somewhere in the flow however, its being told to use require to retrieve from remark-emoji, leading to the break, as remark-emoji does not posses and exports pathway for require.

Switching to next.config.js avoids this issue as we avoid the above flow, allowing import to work as expected.

Issue with the MDX Loader:

Within the plugin options, we can provide one of two values:

The package directly (e.g., import emoji from 'remark-emoji'; then providing emoji). This will break because we encounter the non-serializable issue since the imported plugin is a function.

Or just include the name as a string (['remark-emoji']).

The problem here is that the MDX loader (next-mdx/mdx-js-loader.js) tries to require the plugin directly, which fails because it attempts to require an ES module without a CommonJS entry point, resulting in:

TypeError: Cannot read properties of null (reading 'in')

This happens here:

getOptions found in packages/next-mdx/mdx-js-loader.js. getOptions uses require to resolve from packages, meaning it simply again can't find a pathway to import the package, resulting in 'null' which leads us to the 'in' of null problem.

Next Steps

  • I'd love to know what a plugin output should look like.

  • Perhaps debug the next.config.ts issue or add documentation to clarify.

  • Modify the next-mdx/mdx-js-loader to support both import and require.

  • Consider another way to handle this, perhaps users can have scripts or higher order comps ( idea i have not explored )

Would love to try and resolve this! Or if I've got something off, please let me know! 😄 This was fun!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template. Markdown (MDX) Related to Markdown with Next.js.
Projects
None yet
Development

No branches or pull requests

6 participants