javascriptjsxvite

How to dynamically compile a directory of mdx files before each vite build?


I'm using React and Vite for a project I'm working on.

What I currently need to do is compile and render all of the MDX files in a directory but I don't want to do the actual compilation on the client side and I want to exclude some of the files so that they aren't even in the final build.

I already tried using Vite's glob importing but I don't think that can be used to filter out files based on metadata.

My current plan is to:

  1. Read all of my mdx files into an array
  2. Parse the frontmatter and decide which ones I want to keep accordingly
  3. Run the remaining files through the mdx compiler
  4. Be able to import the array into my app

I have all of it figured out except the last step, but I keep encountering errors.

From my research it appears vite offers both virtual modules and build hooks for executing code before the build.

I currently have tried using virtual modules and it seems like the right solution but I'm just not sure how to get my mdx data exported from a virtual module.

import fs from "fs"
import fm from "front-matter"
import { evaluate } from "@mdx-js/mdx"
import * as runtime from "react/jsx-runtime"
import remarkFrontmatter from "remark-frontmatter"
import remarkMdxFrontmatter from "remark-mdx-frontmatter"
import toSource from "tosource"
const path = "./src/posts"
const rawPosts = fs.readdirSync(path).map(fileName => {
  const file = fs.readFileSync(`${path}/${fileName}`)
  return String(file)
})
const filteredPosts = rawPosts.filter(async file => {
  const frontmatter = fm(file)
  // use frontmatter to decide which files to keep
  return true
})
const finalPosts = await Promise.all(
  filteredPosts.map(async file => {
    const parsed = await evaluate(file, {
      ...runtime,
      remarkPlugins: [
        remarkFrontmatter,
        remarkMdxFrontmatter,
      ],
    })
    return parsed
  })
)
export default function posts() {
  const moduleId = "virtual:posts"
  const resolvedModuleId = "\0" + moduleId

  return {
    name: "my-plugin",
    resolveId(id) {
      if (id === moduleId) {
        return resolvedModuleId
      }
    },
    load(id) {
      if (id === resolvedModuleId) {
        // not sure what to do here
        return ?
      }
    },
  }
}

I already tried JSON.stringifying the array but the function disappears when doing that, so what is a better approach? Should I just be compiling the mdx and not evaluating it yet? In that case I don't know how I would dynamically create multiple virtual modules to import. I realize I could probably use the buildStart hook to run a separate script but I feel like it could be achieved with virtual modules similar to how I have tried so far.


Solution

  • // vite.config.js
    import fs from "fs"
    import path from "path"
    import fm from "front-matter"
    import { compile } from "@mdx-js/mdx"
    import remarkFrontmatter from "remark-frontmatter"
    import remarkMdxFrontmatter from "remark-mdx-frontmatter"
    
    const POSTS_VIRTUAL_ID = "virtual:posts"
    const RESOLVED_POSTS_ID = "\0" + POSTS_VIRTUAL_ID
    
    export default {
      plugins: [
        {
          name: "virtual-mdx-posts",
          resolveId(id) {
            if (id === POSTS_VIRTUAL_ID) {
              return RESOLVED_POSTS_ID
            }
          },
          async load(id) {
            if (id === RESOLVED_POSTS_ID) {
              const postDir = path.resolve("src/posts")
              const files = fs.readdirSync(postDir)
    
              const mdxExports = await Promise.all(
                files.map(async (fileName, index) => {
                  const filePath = path.join(postDir, fileName)
                  const raw = fs.readFileSync(filePath, "utf-8")
                  const { attributes } = fm(raw)
    
                  // Skip some posts based on frontmatter
                  if (attributes.draft) return null
    
                  const compiled = await compile(raw, {
                    outputFormat: "function-body",
                    remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
                  })
    
                  const exportName = `Post${index}`
                  return {
                    name: exportName,
                    code: compiled.value,
                    frontmatter: attributes,
                  }
                })
              )
    
              const validPosts = mdxExports.filter(Boolean)
    
              const moduleCode = validPosts
                .map(
                  ({ name, code, frontmatter }) => `
    export const ${name} = {
      frontmatter: ${JSON.stringify(frontmatter)},
      component: (props) => {
        ${code}
        return MDXContent(props)
      }
    };
    `
                )
                .join("\n")
    
              const exportList = validPosts
                .map(({ name }) => name)
                .join(", ")
    
              return `${moduleCode}\nexport default [${exportList}];`
            }
          },
        },
      ],
    }