kentcdodds / mdx-bundler

🦤 Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Trying to invoke `mdx-bundler` via a custom webpack loader in a Next.js app

brianjenkins94 opened this issue · comments

  • mdx-bundler version: v9.0.1
  • node version: v18.2.0
  • npm version: v8.9.0

Relevant code or config:

// next.config.mjs
export default {
	"pageExtensions": ["js", "jsx", "ts", "tsx", "md", "mdx"],
	"webpack": function(config, options) {
		config.module.rules.push({
			"test": /\.mdx?$/,
			"use": [
				options.defaultLoaders.babel,
				{
					"loader": "./util/webpack/loader.cjs",
					"options": { "theme": "./layouts/docs" }
				}
			]
		});

		return config;
	}
};
// util/webpack/loader.cjs
const path = require("path");

const { bundleMDX } = require("mdx-bundler");

const basePath = path.join(__dirname, "..", "..");

module.exports = async function(source) {
	const callback = this.async();

	this.addContextDependency(path.join(basePath, "pages"));

	const { code, frontmatter } = await bundleMDX({
		"source": source,
	});

	callback(null, `
		import { getMDXComponent } from "mdx-bundler";

		export default function(props) {
			return getMDXComponent("${code}");
		}
	`);
};

What you did:

Attempted to render a MDX file via a custom webpack loader.

What happened:

> Ready on http://localhost:3000
wait  - compiling /path/to/mdx/file (client and server)...
wait  - compiling...
error - ./pages/path/to/mdx/file/index.mdx
Error: 
  x Expected ',', got 'object'
   ,----
 5 | return getMDXComponent("var Component=(()=>{var d=Object.create;var c=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var m=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty;var g=(r,i)=>()=>(i||r((i={exports:{}}).exports,i),i.exports),S=(r,i)=>{for(var o in i)c(r,o,{get:i[o],enumerable:!0})},s=(r,i,o,n)=>{if(i&&typeof i=="object"||typeof i=="function")for(let t of u(i))!p.call(r,t)&&t!==o&&c(r,t,{get:()=>i[t],enumerable:!(n=h(i,t))||n.enumerable});return r};var y=(r,i,o)=>(o=r!=null?d(m(r)):{},s(i||!r||!r.__esModule?c(o,"default",{value:r,enumerable:!0}):o,r)),P=r=>s(c({},"__esModule",{value:!0}),r);var a=g((z,l)=>{l.exports=_jsx_runtime});var D={};S(D,{default:()=>v});var e=y(a());function f(r={}){let{wrapper:i}=r.components||{};return i?(0,e.jsx)(i,Object.assign({},r,{children:(0,e.jsx)(o,{})})):o();function o(){let n=Object.assign({h1:"h1",p:"p",h2:"h2",img:"img",ul:"ul",li:"li",strong:"strong",em:"em",h3:"h3"},r.components);return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.h1,{children:"Introduction"}),`
   :                                                                                                                                                                                                                                                                                                                                                                                         ^^^^^^
   `----

Caused by:
    0: failed to process input file
    1: Syntax Error

Reproduction repository:

WIP

Problem description:

I had a clever idea to copy what Nextra is doing to leverage Next.js's file-system based routing so all my MDX files would be rendered automatically, but I'm clearly not invoking mdx-bundler correctly. I'll work on a minimal sample to reproduce the issue and see what I can figure out.

Here is my minimal sample that reproduces the issue:

nextra-knockoff.zip


And here's an example of what Nextra returns:

import withLayout from "./layouts/docs";
import { withSSG } from "nextra/ssg";

/*@jsxRuntime automatic @jsxImportSource react*/
import { useMDXComponents as _provideComponents } from "@mdx-js/react";

function MDXContent(props = {}) {
	const { "wrapper": MDXLayout } = { ..._provideComponents(), ...props.components };

	return MDXLayout ? <MDXLayout {...props}><_createMdxContent /></MDXLayout> : _createMdxContent();

	function _createMdxContent() {
		const _components = {
			"p": "p",
			"img": "img",
			"h1": "h1",
			"strong": "strong",
			"h2": "h2",
			"h3": "h3",
			"ul": "ul",
			"li": "li",
			..._provideComponents(),
			...props.components
		};

		return <>
			<_components.p>
				<_components.img src="path/to/image.png" alt="alt text" />
			</_components.p>
			{"\n"}
			<_components.h1>{"Introduction"}</_components.h1>
			{"\n"}
			<_components.p>{"Content "}<_components.strong>{"strong content"}</_components.strong>{" and more content."}</_components.p>
		</>;
	}
}

const _mdxContent = <MDXContent />;

export default function NextraPage(props) {
	return withSSG(withLayout({
		"filename": "C:/path/to/page.mdx",
		"route": "/path/to/page",
		"meta": {},
		"pageMap": [/* Giant object with the keys: `name`, `children`, `route` */]
	}, null))({
		...props,
		"children": _mdxContent
	});
}

No dice for:

// util/webpack/loader.cjs
const path = require("path");

const { bundleMDX } = require("mdx-bundler");
const { getMDXComponent } = require("mdx-bundler/client");

const basePath = path.join(__dirname, "..", "..");

module.exports = async function(source) {
	const callback = this.async();

	this.addContextDependency(path.join(basePath, "pages"));

	let { code, frontmatter } = await bundleMDX({
		"source": source,
		"esbuildOptions": function(options, frontmatter) {
			options.minify = false;

			return options;
		},
	});

	const Component = getMDXComponent(code)

	callback(null, Component.toString());
};

either. Yields:

error - Error: The default export is not a React Component in page: "/"

And:

	callback(null, "export default " + Component.toString());

Yields:

ReferenceError: import_jsx_runtime is not defined
  10 |         strong: "strong"
  11 |       }, props.components);
> 12 |       return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, {
     |      ^
  13 |         children: [(0, import_jsx_runtime.jsx)(_components.h1, {
  14 |           children: "Wahoo"
  15 |         }), "\n", "\n", (0, import_jsx_runtime.jsxs)(_components.p, {

Where the Component is:

Component
function MDXContent(props = {}) {
  const { wrapper: MDXLayout } = props.components || {};
  return MDXLayout ? (0, import_jsx_runtime.jsx)(MDXLayout, Object.assign({}, props, {
    children: (0, import_jsx_runtime.jsx)(_createMdxContent, {})
  })) : _createMdxContent();
  function _createMdxContent() {
    const _components = Object.assign({
      h1: "h1",
      p: "p",
      strong: "strong"
    }, props.components);
    return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, {
      children: [(0, import_jsx_runtime.jsx)(_components.h1, {
        children: "Wahoo"
      }), "\n", "\n", (0, import_jsx_runtime.jsxs)(_components.p, {
        children: ["Here's a ", (0, import_jsx_runtime.jsx)(_components.strong, {
          children: "neat"
        }), " demo:"]
      }), "\n", (0, import_jsx_runtime.jsx)(demo_default, {})]
    });
  }
}

So its got something to do with this I guess:

mdx-bundler/src/index.js

Lines 166 to 180 in 8a4e6a6

globalExternals({
...globals,
react: {
varName: 'React',
type: 'cjs',
},
'react-dom': {
varName: 'ReactDOM',
type: 'cjs',
},
'react/jsx-runtime': {
varName: '_jsx_runtime',
type: 'cjs',
},
}),

...but why?

I was pretty sure I tried this, but this:

const path = require("path");

const { bundleMDX } = require("mdx-bundler");
const { getMDXComponent } = require("mdx-bundler/client");

const basePath = path.join(__dirname, "..", "..");

module.exports = async function(source) {
	const callback = this.async();

	this.addContextDependency(path.join(basePath, "pages"));

	let { code, frontmatter } = await bundleMDX({
		"source": source
	});

	const Component = getMDXComponent(code)

	callback(null, "import * as import_jsx_runtime from \"react/jsx-runtime\"; export default " + Component.toString());
};

seems to have done it.

I just can't tell if this is needlessly bundling react/jsx-runtime when it could be available in some other way.

Outstanding questions:

  • Is the above solution needlessly bundling react/jsx-runtime?
  • Does Next.js somehow expose react/jsx-runtime in whatever it ships to the client?
  • Or does Next.js have some way of compiling what it's given to native document.createElement calls during the build process?

The jsx runtime is used in next.js as of the "new"(has been around since next.js 9.5) react transform and it is bundled anyways so you aren't bundling it for no reason

@Markos-Th09

Not sure I follow. Are you saying I should have to import jsx-runtime and that what I’m doing isn’t adding redundant dependencies to the bundle?

@Markos-Th09

Not sure I follow. Are you saying I should have to import jsx-runtime and that what I’m doing isn’t adding redundant dependencies to the bundle?

What I am saying is that importing the jsx runtime is fine and you aren't adding any extra dependencies