mdx-bundler icon indicating copy to clipboard operation
mdx-bundler copied to clipboard

How to handle an imported components imports

Open Adam-Collier opened this issue 3 years ago • 15 comments

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: image

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

Adam-Collier avatar Jun 04 '21 20:06 Adam-Collier

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.

Arcath avatar Jun 05 '21 19:06 Arcath

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

Adam-Collier avatar Jun 06 '21 17:06 Adam-Collier

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.

Arcath avatar Jun 11 '21 08:06 Arcath

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 😁

Adam-Collier avatar Jun 29 '21 17:06 Adam-Collier

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.

deadcoder0904 avatar Jul 08 '21 10:07 deadcoder0904

I got this working to a point.

So there where 3 issues I needed to sort:

  1. Button was imported from a .js file not a .jsx
  2. .css files require esbuild to be given a directory to write to.
  3. 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.

Arcath avatar Jul 12 '21 06:07 Arcath

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)

Adam-Collier avatar Jul 25 '21 15:07 Adam-Collier

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.

vpicone avatar Jul 26 '21 18:07 vpicone

@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.

Arcath avatar Jul 27 '21 19:07 Arcath

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

  1. the file path to the "Boxes" component to the "files" key, and the value has to be the file content as string?

  2. 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

chrislicodes avatar Aug 14 '21 15:08 chrislicodes

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...

city17 avatar Dec 29 '21 10:12 city17

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 avatar Mar 08 '22 08:03 FradSer

@FradSer same error here. Don't really know what to do about it.

melosomelo avatar Mar 30 '22 20:03 melosomelo

@melosomelo same error and I gave up.

FradSer avatar Apr 02 '22 06:04 FradSer

@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.

melosomelo avatar Apr 02 '22 15:04 melosomelo