Skip to content

mermaid remains a hard dependency in 2.5.0, contradicting the plugin docs (adds ~75 MB to every install) #501

@dr00-eth

Description

@dr00-eth

Bug Description

The docs at /docs/plugins and /docs/plugins/mermaid describe mermaid as an opt-in plugin that consumers install and wire up themselves via @streamdown/mermaid. The DiagramPlugin interface, the separate @streamdown/mermaid package, and an in-bundle "plugin missing" fallback all suggest the intended design is pluggable.

However, as of streamdown@2.5.0, mermaid is listed in dependencies in the published package.json, so it is installed unconditionally for every consumer — even ones that never render a diagram and never pass a mermaid plugin to <Streamdown />. This contradicts the documented plugin model and adds roughly 75 MB of install footprint to every project that depends on streamdown.

Inspecting the published 2.5.0 tarball shows the runtime does not actually import mermaid statically — all mermaid usage is routed through the plugin context — so the hard dependency appears to be unintentional rather than load-bearing.

Steps to Reproduce

  1. Create a fresh project and install streamdown:
    npm init -y
    npm i streamdown@2.5.0
  2. Inspect installed size and the resolved mermaid version:
    du -sh node_modules/mermaid node_modules/streamdown
    node -e "console.log(require('streamdown/package.json').dependencies.mermaid)"
  3. Grep the published dist for any static mermaid import:
    grep -RnoE "from ['\"]mermaid['\"]" node_modules/streamdown/dist

Expected Behavior

Per the plugin docs, mermaid should be opt-in. A consumer that does not render mermaid diagrams (and does not pass a mermaid plugin to <Streamdown />) should not have mermaid in their node_modules, lockfile, or supply-chain audit surface. Installing streamdown alone should not pull in mermaid; installing @streamdown/mermaid should.

The in-bundle fallback string — "Mermaid plugin not available. Please add the mermaid plugin to enable diagram rendering." — should be a reachable, supported code path.

Actual Behavior

mermaid@^11.12.2 is listed in dependencies (not peerDependencies, devDependencies, or peerDependenciesMeta.optional) in streamdown@2.5.0's published package.json.

  • As a result, every npm install streamdown unconditionally installs mermaid and its full transitive graph (~75 MB on disk, ~130× the size of streamdown itself at ~580 KB).
  • The "plugin missing" fallback path cannot actually be reached, because mermaid is always resolved as a transitive of streamdown.
  • Meanwhile, dist/chunk-BO2N2NFS.js has zero static imports from 'mermaid'. The only bare-package imports in the built chunk are react, rehype-*, remark-*, unified, unist-util-*, hast-util-to-jsx-runtime, html-url-attributes, marked, clsx, tailwind-merge, remend, and react-dom / react/jsx-runtime. All mermaid usage is routed through the DiagramPlugin context the consumer provides.
  • The only remaining coupling is a single type-only import in dist/index.d.ts:
    import { MermaidConfig } from 'mermaid';
    used to type DiagramPlugin.getMermaid(config?: MermaidConfig). This does not require mermaid at runtime.

Code Sample

Relevant excerpt from `node_modules/streamdown/package.json` (2.5.0):

{
  "name": "streamdown",
  "version": "2.5.0",
  "dependencies": {
    "clsx": "^2.1.1",
    "hast-util-to-jsx-runtime": "^2.3.6",
    "html-url-attributes": "^3.0.1",
    "marked": "^17.0.1",
    "rehype-harden": "^1.1.8",
    "rehype-raw": "^7.0.0",
    "rehype-sanitize": "^6.0.0",
    "remark-gfm": "^4.0.1",
    "remark-parse": "^11.0.0",
    "remark-rehype": "^11.1.2",
    "tailwind-merge": "^3.4.0",
    "unified": "^11.0.5",
    "mermaid": "^11.12.2",
    "unist-util-visit": "^5.0.0",
    "unist-util-visit-parents": "^6.0.0",
    "remend": "1.3.0"
  },
  "devDependencies": {
    "@streamdown/cjk": "1.0.3",
    "@streamdown/math": "1.0.2",
    "@streamdown/mermaid": "1.0.2"
  }
}


Relevant excerpt from `dist/index.d.ts`:

import { MermaidConfig } from 'mermaid';

interface DiagramPlugin {
  getMermaid: (config?: MermaidConfig) => MermaidInstance;
  language: string;
  name: "mermaid";
  type: "diagram";
}

interface PluginConfig {
  cjk?: CjkPlugin;
  code?: CodeHighlighterPlugin;
  math?: MathPlugin;
  mermaid?: DiagramPlugin;
  renderers?: CustomRenderer[];
}


Reproduction output on my machine:

$ du -sh node_modules/mermaid node_modules/streamdown
75M   node_modules/mermaid
580K  node_modules/streamdown

$ node -e "console.log(require('streamdown/package.json').dependencies.mermaid)"
^11.12.2

$ grep -RnoE "from ['\"]mermaid['\"]" node_modules/streamdown/dist
node_modules/streamdown/dist/index.d.ts:4:import { MermaidConfig } from 'mermaid';
# (no matches in any .js file)

Streamdown Version

2.5.0

React Version

19.2.5

Node.js Version

23.7.0

Browser(s)

No response

Operating System

None

Additional Context

Why this matters

  • Install size / cold-start cost. Every consumer (including serverless, edge, Docker images, Lambda layers, CI caches, vercel/pkg-style bundlers) pays ~75 MB of install footprint for a package they may never use. For projects deploying to Cloudflare Workers, Vercel Edge, or Lambda, this is significant.
  • Supply-chain surface area. Mermaid transitively pulls in a large graph (d3, dompurify, cytoscape, etc.) that consumers who don't render diagrams should not have to audit, pin, or patch.
  • Docs / reality mismatch. The plugin docs are explicit that mermaid is opt-in. New users read the plugin guide, wire up @streamdown/mermaid, and are surprised to see mermaid in their lockfile regardless.
  • Defeats the point of the plugin architecture. The DiagramPlugin interface, the runtime fallback string, and @streamdown/mermaid existing as a separate package all suggest the intended design was pluggable. The package.json is the only thing blocking it.

Proposed fix

Move mermaid out of dependencies. A few options, in rough order of preference:

  1. Drop mermaid entirely from streamdown's package.json. Let @streamdown/mermaid own the dep. Re-export MermaidConfig from @streamdown/mermaid (or inline a narrow structural type in streamdown's .d.ts) so streamdown has no type-level reference to mermaid either.
  2. Optional peer dep. Declare mermaid in peerDependencies + peerDependenciesMeta: { mermaid: { optional: true } }. Consumers that want built-in mermaid install it; others don't. This keeps the type import working without forcing installation on npm ≥ 7.
  3. Type-only indirection. If (1) / (2) are too disruptive, at minimum replace import { MermaidConfig } from 'mermaid' with a local structural type so there is no type-level coupling, and document that consumers using @streamdown/mermaid get the real typed config through the plugin package.

Option (1) seems most aligned with how the @streamdown/* split and the plugin docs are already framed.

Environment

  • Installed as dependencies[streamdown] alongside React 19 / React Router v7 on Cloudflare Workers.
  • Verified against the published tarball's dist/ and package.json.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions