mdx-bundler
mdx-bundler copied to clipboard
How to handle an imported components imports
Hi guys, thank you for the great project! I'm currently trying to get mdx-bundler set up with Next.js and I'm running into some issues when it comes to importing components. The imported component in the MDX file is fine but if that import has it's own imports it throws an error. Is there a way to tacke this? I'll show you what I have below
-
mdx-bundler
version: 4.0.1 -
node
version: v14.16.0 -
npm
version: 6.14.11
So I have some code to prepare and get the components like in @Arcath's post https://www.arcath.net/2021/03/mdx-bundler
import { bundleMDX } from 'mdx-bundler';
import path from 'path';
import { existsSync } from 'fs';
import { readdir, readFile } from 'fs/promises';
export const prepareMDX = async (source, files) => {
if (process.platform === 'win32') {
process.env.ESBUILD_BINARY_PATH = path.join(
process.cwd(),
'node_modules',
'esbuild',
'esbuild.exe'
);
} else {
process.env.ESBUILD_BINARY_PATH = path.join(
process.cwd(),
'node_modules',
'esbuild',
'bin',
'esbuild'
);
}
const { code } = await bundleMDX(source, {
files,
});
return code;
};
export const getComponents = async (directory) => {
const components = {};
if (!existsSync(directory)) return components;
const files = await readdir(directory);
console.log(files);
for (const file of files) {
if (file.substr(-3) === 'jsx') {
const fileBuffer = await readFile(path.join(directory, file));
components[`./components/${file}`] = fileBuffer.toString().trim();
}
}
return components;
};
which allows me to import the component in postdir > componentsdir but say I am trying to import this form component
import React, { useState } from 'react';
import Button from '../../../src/components/Button';
import styles from '../form.module.css';
const FormWithStyles = ({ title }) => {
const [content, setContent] = useState({
subject: `Feedback sent from: ${title}`,
email: '',
handle: '',
message: '',
honeypot: '',
accessKey: 'your-access-key',
});
const handleChange = (e) =>
setContent({ ...content, [e.target.name]: e.target.value });
return (
<div className={styles.feedback}>
<p>
Please let me know if you found anything I wrote confusing, incorrect or
outdated. Write a few words below and I will make sure to amend this
blog post with your suggestions.
</p>
<form className={styles.form}>
<label className={styles.message} htmlFor="message">
Message
<textarea
name="message"
placeholder="What should I know?"
onChange={handleChange}
required
/>
</label>
<label className={styles.email} htmlFor="email">
Your Email (optional)
<input type="email" name="email" onChange={handleChange} />
</label>
<label className={styles.handle} htmlFor="handle">
Twitter Handle (optional)
<input type="text" name="handle" onChange={handleChange} />
</label>
<input type="hidden" name="honeypot" style={{ display: 'none' }} />
<Button className={styles.submit} type="button" text="Send Feedback" />
</form>
</div>
);
};
export default FormWithStyles;
An error is thrown because the Button component is trying to be imported from my src/components directory and the styles css module is being from that same directory
This is a screenshot of the error:
and the repo can be found here:
https://github.com/Adam-Collier/portfolio-site/tree/render_mdx
(I am currently migrating over from Gatsby so everything is a bit of a mess atm but it should be easy enough to navigate to a blog page that errors)
I appreciate any guidance you have on this and I hope you can help
Your files
object needs to have a key for each file you import e.g. ../../../src/components/Button
. If your getComponents
functions like the one in my example it only looks for files in the same folder as the mdx file so ./demo
would work whilst ../demo
wont.
I'd reccomend looking at the cwd
option which I now use in place of files
. This tells esbuild wher on the disk your file is and relative imports can be resolved properly.
My code is now this: https://github.com/Arcath/arcath.net-next/blob/main/lib/functions/prepare-mdx.ts which does do more than just cwd
but should be a good place to start.
Hey @Arcath, I changed it to the below from looking at the updated prepareMDX function you shared. However, now I get a Module not found: Can't resolve 'builtin-modules'
error. Quite frankly I have no idea how to fix this because it doesn't reference any files or give any info on what's gone wrong. There is this issue: https://github.com/kentcdodds/mdx-bundler/issues/18 but potential fixes dont fit with my scenario
export const prepareMDX = async (source, options) => {
if (process.platform === 'win32') {
process.env.ESBUILD_BINARY_PATH = path.join(
process.cwd(),
'node_modules',
'esbuild',
'esbuild.exe'
);
} else {
process.env.ESBUILD_BINARY_PATH = path.join(
process.cwd(),
'node_modules',
'esbuild',
'bin',
'esbuild'
);
}
const { directory } = options;
const { code } = await bundleMDX(source, {
cwd: directory,
});
return code;
};
At first I thought it was something CSS modules related since ES build doesnt support them yet but after removing all CSS module imports the issue still persists
Is your sample repo up-to-date? Would love to have a look at this issue.
I'm wondering if your css modules should be passed as globals?
something like:
// bundler
const { code } = await bundleMDX(source, {
cwd: directory,
globals: {
'form-with-styles': 'FormWithStyles'
}
});
//display
import {getMDXComponent} from 'mdx-bundler/client'
import FormWithStyles from './form-with-styles'
const Component = getMDXComponent(code, {FormWithStyles})
//mdx
import FormWithStyles from 'form-with-styles'
# Content
<FormWithStyles />
This would exclude the css and component from the bundle and let you drop it in at runtime. Saving the need to bundle it when presumably the gatsby bundle has it already.
Hey @Arcath I've only just seen that you replied! Apologies about that. I've just updated that branch so it is up to date with what I had for you 😁
Module not found: Can't resolve 'builtin-modules' error
@Adam-Collier this error can be solved easily. You basically cannot export Node.js modules into client-side code & it throws error if you're using a Barrel file.
I got this working to a point.
So there where 3 issues I needed to sort:
-
Button
was imported from a.js
file not a.jsx
-
.css
files require esbuild to be given a directory to write to. -
next/link
can't be bundled.
1 was easy, just renamed index.js
to index.jsx
.
2 needed the post object to supply some extra details:
// bring all of the data together
const postData = {
...data,
slug,
title,
content,
name,
directory: join(root, baseDir, name),
publicDirectory: join(root, 'public', baseDir, name)
};
Then pass these to prepareMDX
and use them in the bundleMDX
like so
const { code } = await bundleMDX(source, {
cwd: directory,
globals: {
'next/link': '_next_link'
},
esbuildOptions: options => {
options.write = true
options.outdir = publicDirectory
return options
}
});
3 was solved in the above code by saying next/link
should be set the value of the variable _next_link
and then passing the imported Link
to getMDXComponent
const Component = useMemo(() => getMDXComponent(source, {'_next_link': Link}), [source]);
That got it working, although the outputted css is not linked anywhere.
Might be better to lift Button
into the globals instead as I assume its used elsewhere in the site.
Looking at how you do css I don't think you'll get away from having your mdx bundles output css.
Hey @Arcath thanks for taking the time to look into this! Ok cool, that all makes sense, I'll try and get something working with it. When it comes to outputting CSS would it make more sense to use Styled Components or some other css-in-js solution? (I was thinking of moving away from styles components anyway)
I think I'm struggling with the same issue re: CSS modules. Setting the outdir
stopped the build errors, but my the import is just an empty object. Presumably I need to enable css modules through the esmodule options but I was unsuccessful.
@Adam-Collier If you have any css generated by esbuild its going to output a .css file in your bundle which I don't think Next.JS will be happy with. As far as I am aware Next.JS only lets you import css in the _app.js file.
I'd say your best bet is to supply any css components at run time so they are not in the bundle and thus wouldn't output any css.
Hi @Arcath - this thread was already helpful, but I still have some trouble understanding:
Lets say I want to blog about Three.js using React-Three-Fiber, example Component:
The Box:
import { useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { Box as NativeBox } from "@react-three/drei";
export default function Box(props) {
const mesh = useRef(null);
const [hovered, setHover] = useState(false);
const [active, setActive] = useState(false);
useFrame(() => (mesh.current.rotation.x = mesh.current.rotation.y += 0.01));
const color = props.color || "#720b23";
return (
<NativeBox
args={[1, 1, 1]}
{...props}
ref={mesh}
scale={active ? [6, 6, 6] : [5, 5, 5]}
onClick={() => setActive(!active)}
onPointerOver={() => setHover(true)}
onPointerOut={() => setHover(false)}
>
<meshStandardMaterial
attach="material"
color={hovered ? "#2b6c76" : color}
/>
</NativeBox>
);
}
The Component:
import React from "react";
import { Canvas } from "@react-three/fiber";
import Box from "src/components/labs/ripple/Box";
import { OrbitControls } from "@react-three/drei";
function AnimationCanvas({ color }) {
return (
<Canvas camera={{ position: [100, 10, 0], fov: 35 }}>
<ambientLight intensity={2} />
<pointLight position={[40, 40, 40]} />
<Box position={[10, 0, 0]} color={color} />
<Box position={[-10, 0, 0]} color={color} />
<Box position={[0, 10, 0]} color={color} />
<Box position={[0, -10, 0]} color={color} />
<OrbitControls />
</Canvas>
);
}
const Boxes = ({ color }) => {
return <AnimationCanvas color={color} />;
};
export default Boxes;
In my MDX, I would write something like
---
title: "Post with some React Three Fiber"
publishedAt: "2021-07-13"
summary: "Test Summary"
description: "Test Description"
draft: "false"
tags: ["React"]
seoImage: "some link"
---
import Boxes from "../../components/Boxes"
# This should be h1
Commodo ut qui proident anim minim adipisicing irure elit
> Sit laborum est ullamco id occaecat sunt laborum ullamco.
## This should be h2
<Boxes />
So I have to add
-
the file path to the "Boxes" component to the "files" key, and the value has to be the file content as string?
-
What do I now have to add to globals? Everything which I dont want to be bundled - so it would look something like this (I am also following your examples incl. prepareMDX):
const { code } = await bundleMDX(source, {
cwd: directory,
globals: {
'@react-three/fiber': '_react_three_fiber',
'@react-three/drei': '_react_three_drei',
'three': '_three',
},
files: {
"../../components/Boxes": "here the file content as string"
},
esbuildOptions: options => {
options.write = true
options.outdir = publicDirectory
return options
}
});
import fiber from '@react-three/fiber';
import drei from '@react-three/drei';
import THREE from 'three';
const Component = useMemo(
() =>
getMDXComponent(source, {
_react_three_fiber: fiber,
_react_three_drei: drei,
_three: THREE,
}),
[source]
);
Would that be correct? I will create a repo and try around, and I will share the link a bit later - thanks in advance for taking the time
I'm facing the same issue where I'm trying to use a component in an MDX file, which in turn imports other components and a CSS module. So:
// This is the MDX File
import Arrow from './Arrow.jsx'
<Arrow />
// This is the component file
import { motion } from "framer-motion"
import styles from "./Arrow.module.css"
const Arrow = () => {
return (
<div className={styles.arrow}>➔</div>
)
}
export default Arrow
Did anyone find a definitive solution to this use case? Or is it best to use CSS in JS instead of CSS modules here? Would prefer to use modules since that's what I'm using everywhere else on my blog...
In my case it throw
ReferenceError: Can't find variable: process
// index.mdx
import Topography from './Topography';
<Topography />
export const ROOT = process.cwd();
const getCompiledMDX = async (content: string) => {
// ...
try {
return await bundleMDX({
source: content,
xdmOptions(options) {
options.remarkPlugins = [
...(options.remarkPlugins ?? []),
...remarkPlugins,
];
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
...rehypePlugins,
];
return options;
},
cwd: POSTS_PATH,
});
} catch (error: any) {
throw new Error(error);
}
// ...
}
@FradSer same error here. Don't really know what to do about it.
@melosomelo same error and I gave up.
@FradSer, I opened an issue about it. I managed to make it work through some esbuild configurations, but idk if it's a very solid solution. Waiting on a response.