kentcdodds / mdx-bundler

🦤 Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!
MIT License
1.78k stars 75 forks source link

Nextjs and Storybook: ModuleNotFoundError: Module not found: Error: Can't resolve 'fs' #146

Open talohana opened 2 years ago

talohana commented 2 years ago

What you did:

I am building my site with Nextjs and using mdx-bundler for my blog. I was trying to add storybook for my blog page, which loads the posts using mdx-bundler.

The blog loads the posts in a similar way as the reproduction below.

What happened:

Starting storybook I get the following error, followed by more similar errors (which seems to be related to esbuild):

ModuleNotFoundError: Module not found: Error: Can't resolve 'fs' in 'C:\example\node_modules\resolve\lib'
    at C:\example\node_modules\webpack\lib\Compilation.js:925:10
    at C:\example\node_modules\webpack\lib\NormalModuleFactory.js:401:22
    at C:\example\node_modules\webpack\lib\NormalModuleFactory.js:130:21
    at C:\example\node_modules\webpack\lib\NormalModuleFactory.js:224:22
    at C:\example\node_modules\neo-async\async.js:2830:7
    at C:\example\node_modules\neo-async\async.js:6877:13
    at C:\example\node_modules\webpack\lib\NormalModuleFactory.js:214:25
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\Resolver.js:213:14
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\Resolver.js:285:5
    at eval (eval at create (C:\example\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:15:1)
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\UnsafeCachePlugin.js:44:7
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\Resolver.js:285:5
    at eval (eval at create (C:\example\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:15:1)
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\Resolver.js:285:5
    at eval (eval at create (C:\example\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:27:1)
    at C:\example\node_modules\webpack\node_modules\enhanced-resolve\lib\DescriptionFilePlugin.js:67:43

After commenting getStaticProps at Home component, storybook runs as expected.

Reproduction repository:

https://codesandbox.io/s/nextjs-storybook-mdx-bundler-2jheo?file=/pages/index.js

Running yarn dev in terminal or restarting sandbox will raise the error at the terminal

andre-brdoch commented 2 years ago

Late answer, but maybe it will be helpful for someone else:

Storybook is a client side application, so Node.js modules like fs do not work. Therefore, mdx-bundler can not be called from within storybook.

What you can do is doing the bundeling in a separate script:

// bundle-mdx.js
const { bundleMDX } = require('mdx-bundler')
const fs = require('fs-extra')
const { join } = require('path')

;(async function () {
  const sourceDir = join(__dirname, 'mdx')
  const sourceFiles = await fs.readdir(sourceDir, 'utf-8')
  const targetDir = join(__dirname, 'bundled')

  await fs.remove(targetDir)
  await fs.ensureDir(targetDir)

  await Promise.all(
    sourceFiles.map(async file => {
      const sourceFilePath = join(sourceDir, file)
      const source = await fs.readFile(sourceFilePath, 'utf-8')
      const bundled = await bundleMDX({ source })
      const json = JSON.stringify(
        bundled,
        undefined,
        process.env.NODE_ENV === 'production' ? null : 2
      )
      const targetFile = join(targetDir, file.replace(/\.mdx$/, '.json'))
      await fs.writeFile(targetFile, json)
    })
  )
})()

And add to your package.json:

{
"scripts": {
    "bundle-story-mdx": "node ./bundle-mdx.js",
    "bundle-story-mdx:watch": "nodemon --watch ./mdx -e md,mdx ./bundle-mdx.js",
  },
 ...
}

Create a mdx folder next to bundle-mdx.js. When you run npm run bundle-story-mdx (or yarn bundle-story-mdx, if you are using yarn), all mdx files within will get bundled by the script.

To automatically run the script whenever you change a mdx file, install nodemon via npm i -D nodemon, and run npm run bundle-story-mdx:watch.

In storybook, you can then just import the bundles and use them in your stories:

// MyComponent.stories.ts
import React from 'react'
import { Meta, Story } from '@storybook/react/types-6-0'
import { MyComponent, MyComponentProps } from './MyComponent'
import bundle from './bundled/example.json'

const meta: Meta = {
  title: 'MyComponent',
  component: MyComponent,
}
export default meta

const Template: Story<MyComponentProps> = args => <MyComponent {...args} />

export const Standard = Template.bind({})
Standard.args = {
  // pass bundle down to your components
  bundledMdx: bundle.code,
}

You might have to adjust things to make it work with your Next.js page.