Skip to content
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

How to handle an imported components imports #53

Open
Adam-Collier opened this issue Jun 4, 2021 · 15 comments
Open

How to handle an imported components imports #53

Adam-Collier opened this issue Jun 4, 2021 · 15 comments

Comments

@Adam-Collier
Copy link

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

@Arcath
Copy link
Collaborator

Arcath commented Jun 5, 2021

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.

@Adam-Collier
Copy link
Author

Adam-Collier commented Jun 6, 2021

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

@Arcath
Copy link
Collaborator

Arcath commented Jun 11, 2021

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.

@Adam-Collier
Copy link
Author

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 😁

@deadcoder0904
Copy link

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.

@Arcath
Copy link
Collaborator

Arcath commented Jul 12, 2021

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.

@Adam-Collier
Copy link
Author

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)

@vpicone
Copy link

vpicone commented Jul 26, 2021

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.

@Arcath
Copy link
Collaborator

Arcath commented Jul 27, 2021

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

@chrislicodes
Copy link

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

@city17
Copy link

city17 commented Dec 29, 2021

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

@FradSer
Copy link

FradSer commented Mar 8, 2022

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);
  }
// ...
}

@melosomelo
Copy link

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

@FradSer
Copy link

FradSer commented Apr 2, 2022

@melosomelo same error and I gave up.

@melosomelo
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants