Skip to content

[Bug]: <Meta of={} /> fails in production build when CSF4 story module is split into a separate chunk #34373

@yuheiy

Description

@yuheiy

Describe the bug

When using CSF4 stories (preview.meta() / meta.story()) without export default together with MDX docs files that reference them via <Meta of={Stories} />, production builds (storybook build) fail at runtime with:

screenshot of error in storybook
Error: <Meta of={} /> must reference a CSF file module export or meta export.
Did you mistakenly reference your component instead of your CSF file?

This only occurs in production builds when the bundler splits the story module into a separate chunk. storybook dev works correctly.

Reproduction link

https://github.com/yuheiy/storybook-mdx-meta-without-default-export-bug

Reproduction steps

npm install
npm run build-storybook
npx http-server storybook-static
# Open http://localhost:8080/iframe.html?viewMode=docs&id=button--docs

Key files:

  • src/Button.stories.tsx — CSF4 story without export default meta, with top-level await (async module)
  • src/Button.mdx — MDX doc page using <Meta of={ButtonStories} />
  • .storybook/main.ts — uses codeSplitting.groups to force the story module into a separate chunk (simulates what happens naturally in large projects with many shared dependencies)

System

│

│  Storybook Environment Info:
│

│  System:
│  OS: macOS 15.7.4
│  CPU: (10) arm64 Apple M1 Max
│  Shell: 5.9 - /bin/zsh
│  Binaries:
│  Node: 24.14.0 -
│  /Users/yuhei.yasuda/.local/share/mise/installs/node/24.14.0/bin/node
│  Yarn: 2.4.3 -
│  /Users/yuhei.yasuda/.local/share/mise/installs/node/24.14.0/bin/yarn
│  npm: 11.9.0 -
│  /Users/yuhei.yasuda/.local/share/mise/installs/node/24.14.0/bin/npm
│  <----- active
│  pnpm: 10.33.0 - /Users/yuhei.yasuda/Library/pnpm/pnpm
│  Browsers:
│  Chrome: 146.0.7680.165
│  Firefox: 148.0
│  Safari: 26.3.1
│  npmPackages:
│  @storybook/addon-docs: ^10.3.3 => 10.3.3
│  @storybook/react-vite: ^10.3.3 => 10.3.3
│  storybook: ^10.3.3 => 10.3.3

Additional context

Root cause

DocsContext.resolveModuleExport() uses a Map keyed by object identity (===) to look up CSF files:

// referenceCSFFile registers the module namespace object
referenceCSFFile(csfFile) {
  this.exportsToCSFFile.set(csfFile.moduleExports, csfFile);         // full namespace
  this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile); // default export
}

// resolveModuleExport looks it up
resolveModuleExport(moduleExportOrType) {
  let csfFile = this.exportsToCSFFile.get(moduleExportOrType);
  // Fallback: check default export
  if (!csfFile && moduleExportOrType && typeof moduleExportOrType == "object"
      && "default" in moduleExportOrType) {
    csfFile = this.exportsToCSFFile.get(moduleExportOrType.default);
  }
  if (csfFile) return { type: "meta", csfFile };
  // Falls through to { type: "component" } → error
}

When the bundler splits a story module into a chunk file + re-export file:

  1. Storybook's runtime dynamically imports the re-export file → gets namespace object A
  2. referenceCSFFile() registers A in the Map
  3. The compiled MDX statically imports from the chunk file → gets a different namespace object B
  4. <Meta of={B} /> calls resolveModuleExport(B)Map.get(B) fails because B !== A

The fallback checks "default" in B, but CSF4 modules have no export default, so the fallback is also ineffective.

Workaround

Add export default meta to CSF4 story files. This enables the existing fallback in resolveModuleExport because both namespace objects (A.default and B.default) reference the same meta object instance.

  const meta = preview.meta({ component: Button });
+ export default meta;

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