Skip to content

[Bug]: storybook broken when app itself uses MDX #33362

@thediveo

Description

@thediveo

Describe the bug

Summary

Given the following SPA (https://github.com/thediveo/lxkns/tree/chore/web/web/lxkns ... notice the chore/web branch) using...

  • React 19
  • MUI 7
  • mdx-js 3
  • Vite 7
  • Storybook 10

...crashes with...

■  Vite Internal server error: Unexpected `FunctionDeclaration` in code: only
│  import/exports are supported
│  Plugin: @mdx-js/rollup
│  File: ./src/components/helpviewer/chapters/01-intro.mdx:3:1
│  1  |  import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from
│  "react/jsx-runtime";
│  2  |  import {useMDXComponents as _provideComponents} from
│  "file://./node_modules/@storybook/addo...
│  3  |  function _createMdxContent(props) {
│  |   ^
│  4  |    const _components = {
│  5  |      h1: "h1",

...as soon as a component uses MDX for itself.

Configuration

main.ts

Notice, stories does not include a pattern for .mdx content.

// .storybook/main.ts
import type { StorybookConfig as StorybookViteConfig } from '@storybook/react-vite'

const config: StorybookViteConfig = {
    framework: {
        name: '@storybook/react-vite',
        options: {},
    },

    stories: [
        '../src/**/*.stories.@(ts|tsx)',
    ],

    addons: [
        '@storybook/addon-docs',
        '@storybook/addon-links',
        '@storybook/addon-themes',
    ],

    docs: {
        defaultName: 'Description',
    },

    core: {
        disableTelemetry: true,
        disableWhatsNewNotifications: true,
    },

    typescript: {
        check: true,
    },
}

export default config

proview.tsx

// .storybook/preview.tsx
import { BrowserRouter } from 'react-router-dom'

import type { Parameters, Preview } from '@storybook/react-vite'

import { withThemeFromJSXProvider, DecoratorHelpers } from '@storybook/addon-themes'
const { pluckThemeFromContext } = DecoratorHelpers

import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'
import '@fontsource/roboto-mono/400.css'

import { lxknsDarkTheme, lxknsLightTheme } from '../src/app/appstyles'
import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { themes } from 'storybook/theming'


const lightTheme = createTheme(
    {
        components: {
            MuiSelect: {
                defaultProps: {
                    variant: 'standard', // MUI v4 default.
                },
            },
        },
        palette: {
            mode: 'light',
            primary: {
                main: '#3f51b5',
            },
            secondary: {
                main: '#f50057',
            },
        },
    },
    lxknsLightTheme,
)

const darkTheme = createTheme(
    {
        components: {
            MuiSelect: {
                defaultProps: {
                    variant: 'standard', // MUI v4 default.
                },
            },
        },
        palette: {
            mode: 'dark',
        },
    },
    lxknsDarkTheme,
)

export const parameters: Parameters = {
    docs: {
        theme: themes.normal, // use same theme as the surrounding parts
    },
}

const preview: Preview = {
    decorators: [
        (Story) => (
            <BrowserRouter basename=''>
                <StyledEngineProvider injectFirst>
                    <Story />
                </StyledEngineProvider>
            </BrowserRouter>
        ),
        withThemeFromJSXProvider({
            themes: {
                light: lightTheme,
                dark: darkTheme,
            },
            defaultTheme: themes.normal.base,
            Provider: ThemeProvider,
            GlobalStyles: CssBaseline,
        }),
        (Story, context) => {
            const isDark = pluckThemeFromContext(context) === 'dark'
            // addon-docs is broken ... nein! doch! ohhh! ... and thus we need
            // to deal with duplicates. ohhh!
            const docsStories = document.querySelectorAll(`#anchor--${context.id} .docs-story`) as 
                NodeListOf<HTMLElement>
            docsStories.forEach((story) => {
                if (story.style) {
                    story.style.background = isDark
                        ? darkTheme.palette.background.default
                        : lightTheme.palette.background.default;
                }
            })
            return <Story />
        },
    ],
}

export default preview

vite.config.ts

This configuration works correctly with mdx-js 3.

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import svgr from 'vite-plugin-svgr'
import mdx from '@mdx-js/rollup'
import path from 'path'

import remarkGfm from 'remark-gfm'
import remarkImages from 'remark-images'
import remarkTextr from 'remark-textr'
import remarkGEmoji from 'remark-gemoji'

import textrTypoApos from 'typographic-apostrophes'
import textrTypoQuotes from 'typographic-quotes'
import textrTypoPossPluralsApos from 'typographic-apostrophes-for-possessive-plurals'
import textrTypoEllipses from 'typographic-ellipses'
import textrTypoNumberEnDashes from 'typographic-en-dashes'

import rehypeSlug from 'rehype-slug'

// There's only typographic-em-dashes that covers US typographic style, but no
// need for a full-blown npm module just to get European en dash typography.
const textrTypoEnDashes = (input: string) => {
    return input
        .replace(/ -- /gim, ' – ')
}

const srcs = [
    'app',
    'components',
    'hooks',
    'icons',
    'models',
    'utils',
    'views',
]

// https://vite.dev/config/
export default defineConfig({
    base: './',
    build: {
        outDir: 'build'
    },
    server: {
        host: "0.0.0.0",
        port: 3300,
        proxy: {
            '/api': 'http://localhost:5010',
        },
    },
    resolve: {
        alias: Object.fromEntries(
            srcs.map(d => [d, path.resolve(__dirname, `src/${d}`)])
        )
    },
    plugins: [
        {
            enforce: 'pre',
            ...mdx({
                remarkPlugins: [
                    remarkGfm,
                    remarkImages,
                    remarkGEmoji,
                    [remarkTextr, {
                        plugins: [
                            textrTypoApos,
                            textrTypoQuotes,
                            textrTypoPossPluralsApos,
                            textrTypoEllipses,
                            textrTypoNumberEnDashes,
                            textrTypoEnDashes,
                        ],
                        options: {
                            locale: 'en-us'
                        }
                    }],
                ],
                rehypePlugins: [
                    rehypeSlug,
                ],
            })
        },
        tsconfigPaths(),
        react(),
        svgr({
            svgrOptions: {
                icon: true,
            }
        }),
    ]
})

tsconfig.json

// tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

tsconfig.app.json

// tsconfig.app.json
{
  "compilerOptions": {
    "baseUrl": "./src",

    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": [
      "ES2022", 
      "DOM", 
      "DOM.Iterable"
    ],
    "module": "ESNext",
    "types": [
      "react",
      "vite/client"
    ],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": false,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": [
    "src",
    "vite.config.ts"
  ]
}

tsconfig.node.json

// tsconfig.node.json
{
  "compilerOptions": {
    "baseUrl": "./src",

    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2023",
    "lib": [
      "ES2023",
      "dom"
    ],
    "module": "ESNext",
    "types": [
      "node",
      "vite/client"
    ],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": false,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": [
    "src",
    "vite.config.ts"
  ]
}

...how to make storybook work with SPAs that use mdx themselves?

Reproduction link

https://github.com/thediveo/lxkns/tree/chore/web/web/lxkns

Reproduction steps

  1. git clone -b chore/web https://github.com/thediveo/lxkns.
  2. open the workspace in a dev container.
  3. cd web/lxkns
  4. yarn && yarn run storybook
  5. navigate to: http://localhost:6006/?path=/docs/universal-helpviewer--description
  6. crashes and burns (see details above)

System

│
│  
│  Storybook Environment Info:
│

│  System:
│  OS: Linux 6.14 Ubuntu 24.04.3 LTS 24.04.3 LTS (Noble Numbat)
│  CPU: (4) x64 Intel(R) Xeon(R) Gold 6354 CPU @ 3.00GHz
│  Shell: 5.2.21 - /bin/bash
│  Binaries:
│  Node: 24.12.0 - /usr/local/share/nvm/versions/node/v24.12.0/bin/node
│  Yarn: 1.22.22 - /usr/bin/yarn <----- active
│  npm: 11.6.2 - /usr/local/share/nvm/versions/node/v24.12.0/bin/npm
│  pnpm: 10.25.0 - /usr/local/share/nvm/versions/node/v24.12.0/bin/pnpm
│  npmPackages:
│  @storybook/addon-docs: ^10.1.4 => 10.1.9
│  @storybook/addon-links: ^10.1.4 => 10.1.9
│  @storybook/addon-themes: ^10.1.8 => 10.1.9
│  @storybook/react-vite: ^10.1.4 => 10.1.9
│  storybook: ^10.1.9 => 10.1.9

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions