Skip to content

[pull] develop from mermaid-js:develop - #234

Merged
pull[bot] merged 47 commits into
code:developfrom
mermaid-js:develop
Sep 3, 2026
Merged

[pull] develop from mermaid-js:develop#234
pull[bot] merged 47 commits into
code:developfrom
mermaid-js:develop

Conversation

@pull

@pull pull Bot commented Sep 3, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

ashishjain0512 and others added 30 commits August 28, 2026 10:23
ELK previously shipped as a separate `@mermaid-js/layout-elk` package that
sites had to install and register. Its source now lives in core at
`rendering-util/layout-algorithms/elk`, is registered automatically, and is
the default `layout`.

Flowchart, state, class, ER, requirement, use-case and agentflow diagrams
that do not specify a `layout` are now laid out by ELK instead of dagre.
Mindmap keeps dagre unless a layout is explicitly requested, which needed a
way to tell "the user asked for this" from "this is just the default" —
`isConfigKeySet` provides it on top of the existing `getUserDefinedConfig`.

Bundling:

- ELK registers behind `injected.includeLargeFeatures` as a lazy import, so
  the ESM builds fetch it only when a diagram uses it. The single-file IIFE
  build inlines it and grows by ~516 kB gzipped.
- The tiny build omits ELK and falls back to dagre; its size is unchanged.
- `@mermaid-js/layout-elk` remains published and is now built from core's
  ELK source, so there is one implementation. Because it no longer imports
  the whole `mermaid` entry point, its payload drops from ~1.58 MB to
  ~728 kB gzipped. It is how the tiny build gets ELK.

Also:

- Drops the cross-bundle `mermaid.mermaidAPI.setConfig` hack, which only
  existed because the separate bundle carried a second mermaid instance.
- Routes the state renderer through `getRegisteredLayoutAlgorithm`, which
  it previously bypassed, so an unavailable layout falls back rather than
  failing to render.
- Pins the e2e suite to dagre in `mermaidUrl` so existing visual baselines
  are preserved; diagrams that select ELK by syntax opt out, since the
  detector's choice ranks below a user-supplied layout.
- Guards `Object.keys(dependencies)` in the esbuild config, which crashed
  for a package with no dependencies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two CI failures from bundling ELK.

build-docs: the docs site aliases `mermaid` to `dist/mermaid.esm.min.mjs`,
and Vite runs `es-module-lexer` over every module for import analysis. The
minified ELK chunk is the one module in the graph that lexer rejects, which
surfaced as "content contains invalid JS syntax". The unminified ESM build
lexes clean (verified across all 136 modules of its graph), and VitePress
minifies its own output anyway, so the pre-minified input bought nothing.

lint: `layout`'s longer schema description shifted line numbers in the
generated config types, so four typedoc pages were stale — the source links
in `MermaidConfig`, `LayoutLoaderDefinition`, `RenderOptions` and
`getEffectiveHtmlLabels`. Regenerated; the diff is line numbers only.

Note for anyone regenerating docs from a git worktree: typedoc requires
`.git` to be a directory to discover the repository, and in a worktree it is
a file, so it silently emits every "Defined in:" without its source link.
The links here were restored to match what CI produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…layout

The baseline pin set `layout: dagre` for any spec that did not ask for one,
which outranks a diagram type's own choice — so the whole swimlanes suite
rendered with dagre and lost its lanes (33 failures on `g.cluster.swimlane`
resolving to 0 elements).

`swimlanes` is `createFlowDiagram({ defaultLayout: 'swimlane' })`, and
`flowDiagram.init` ranks `defaultLayout` below a user-supplied layout —
exactly the precedence that `flowchart-elk` already needed an opt-out for. I
only special-cased the ELK half of it.

Detect this from the diagram source in `mermaidUrl` instead of relying on
each spec to pass `useDiagramLayout`, because a missed case does not fail
loudly: the swimlanes fixtures under `e2e/diagrams` render through the
screenshot-only mmd runner, where a dagre-laid-out swimlanes diagram would
have been silently baselined as correct.

`useDiagramLayout` stays for `flowchart.defaultRenderer: 'elk'`, which is
not detectable from the source — only `flowDiagram.init` promotes it, so it
is inert on the class/er/mindmap fixtures that carry it and those must stay
pinned.

Extends the pin regression spec to cover self-selecting diagrams, multi-graph
renders, and a flowchart that merely mentions the keyword in a node label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite was pinned to dagre in `mermaidUrl` to keep existing Argos
baselines comparable. That hides the layout users will actually get, so
remove it and let every diagram render with ELK — the baselines are meant to
move.

Removes the pin, the `useDiagramLayout` opt-out and the self-selecting-layout
detection it needed, plus the regression spec guarding all of it. `e2e/` is
now byte-identical to before this branch.

Also drops `elk-default.spec.ts`. It existed only to cover the shipped
default that the pin was hiding; with no pin, every spec exercises it.

Argos will show a large diff across the whole suite. That is the intent: it
is the review surface for how diagrams look under ELK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`usecase.docs.spec.ts` renders all 20 mermaid-example fences from the
canonical page through a real layout engine in jsdom. ELK, now the default,
does considerably more work than dagre and pays a one-off cost to load
elkjs — roughly 55ms per example locally, which on a slower CI runner pushes
the whole spec past vitest's 5s default. It timed out in CI while passing
locally in 1.1s.

Adds an optional timeout passthrough to `jsdomIt` and uses 30s here. Left as
a real render rather than pinned to dagre: the point of the spec is that the
documented examples render under the layout users actually get.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-flowchart-layout-4d2342

# Conflicts:
#	docs/config/setup/mermaid/interfaces/MermaidConfig.md
Prettier collapses the two-symbol import onto one line; my hand-edit after
moving the file into core left it expanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mindmap defaults to `cose-bilkent`, and `mindmapDb.getData()` already decides
that: when `getUserDefinedConfig().layout` is undefined it stamps
`cose-bilkent` onto the config it hands the renderer.

I missed that when making ELK the global default, read only the renderer, and
concluded mindmap was effectively on dagre. The override I added then forced
dagre unconditionally, so mindmaps silently changed layout engine. Reverting
`mindmapRenderer.ts` to its original form restores the correct behaviour and
needs nothing new: the db's existing check is unaffected by the global
default changing, because an unset `layout` is still unset.

Also drops `isConfigKeySet`. It was added solely for that override and
duplicated the `getUserDefinedConfig()` idiom `mindmapDb` was already using,
so it is now unused public API.

Replaces the spec that codified the wrong behaviour with one that guards the
right one — mindmap resolves to `cose-bilkent` while the global default is
`elk`, and still honours an explicit layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…efault

Two failures from the e2e run, both real.

**arrowMarkerAbsolute ignored under ELK.** The e2e viewer registered the
standalone `@mermaid-js/layout-elk` bundle, which overrides mermaid's own
registration with a second, self-contained copy whose config module never
sees `initialize()` — so edges painted with schema defaults. That is exactly
what `syncElkPackageConfig` existed for; I removed it believing
`setConfig(getConfig())` was an identity round-trip. It is not: the local
`setConfig` and the host's `getConfig` are different module instances in the
plugin build, so it copies host config into the plugin's copy.

Restored as `syncHostConfig`, using a relative `setConfig` rather than the
package's default export, so it repairs the plugin build and is a genuine
no-op in core. The e2e viewer no longer registers the package either, so the
suite exercises the ELK users actually get instead of the plugin build.

**State diagrams with notes failed to render.** `note right of <composite>`
produced a note node carrying `type`/`isGroup` inherited from the state it
annotates, marking the note as a container. The shared paint path then looked
up a `note` *cluster* shape, which does not exist, and threw
`shapes[shape] is not a function`, blanking the whole diagram. A note is
always a leaf inside the note group, so it is now `isGroup: false`.

Pre-existing: dagre only reads `isGroup` for edge hints, so it rendered
regardless. It surfaces now that state defaults to ELK, and would already
reproduce on `layout: elk`. Covered by a unit test that fails without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`syncHostConfig` looks like a no-op from inside mermaid — the host's config
module and the layout's are the same module there, so removing the call changes
nothing that any existing test observes. It only matters in the standalone
`@mermaid-js/layout-elk` bundle, which carries its own copy of the config module
that never sees the host's `initialize()`. That asymmetry is why the call was
removed in the first place when ELK moved into mermaid.

The spec asserts the CALL rather than the resulting config value, because a
value assertion would pass either way in-process and would give false
confidence. Both cases fail if the call is removed: that the host's config is
handed over, and that it happens before the layout data is read, since
`measureLayoutFn` builds the labels after `prepareLayout` returns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e edge

The #8194 fix repointed a degenerately-anchored edge at the small node's
centre. But ELK, with nodeFlexibility PORT_POSITION, had already placed
the node so its clamped port sat on a straight route — the node was what
stood off-centre, not the edge — so the repointing bent straight
verticals visibly diagonal in every fixture with an edge between a state
dot and a wider node.

Replace the drop-the-anchor logic with an alignment pre-pass: before any
edge points are built, a node whose terminal anchor sits on a degenerate
side (shorter than twice the ports-surrounding margin) is shifted along
that side's axis until its centre lands on the anchor line. The edge
stays exactly as routed, the appended centre point is collinear with it,
and running as a pre-pass keeps every edge's view of the node position
consistent regardless of processing order. First anchor wins; the nodeDb
entry and the layout node move together so painting and clipping agree.
fix(elk): move small nodes onto the routed line instead of bending the edge
…agre

Review verdict approved, and fixed some of the comments. Update of baseline will occur later
knsv and others added 17 commits September 2, 2026 14:55
Develop added specs assuming dagre is the default layout and that elk is
the canonical unregistered algorithm; both premises change on this branch.
Expectations follow the elk default, the fallback specs use a genuinely
unregistered name, and the stale ships-separately comments are updated.
…comment

- ER docs: Dagre example actually uses dagre; drop the obsolete 9.4+/lazy-load note
- syntax-reference: list every diagram type that supports layout/look selection,
  with the mindmap cose-bilkent exception
- layouts.md: mindmaps default to cose-bilkent, not dagre
- render.spec: literal-key form for the dotted flat key; evenGroupFrames suite
  moved out of clearContainerAlgorithmOptions
- hostConfigSync.spec: instrument the property the code actually reads (edges)
  and assert the sync-before-read ordering both ways
- elkOptionCatalogue: the catalogue objects are merged too, so stop calling
  them inert; all four blocks must ship empty
…, lexer note

- @mermaid-js/layout-elk declares dompurify, katex, dayjs and dagre-d3-es, so
  the npm core build externalizes them (verified in the built chunks: bare
  imports instead of inlined copies) and they dedupe against the host and
  appear in audits. The self-contained CDN build is unchanged.
- syncHostConfig documents that setConfig sanitizes away the secure keys, and
  the hostConfigSync fixture no longer asserts a securityLevel propagation
  that sanitize() prevents.
- The elk-default-layout changeset records the mermaid.esm.min.mjs
  es-module-lexer incompatibility and corrects the mindmap default to
  cose-bilkent; the layout-elk changeset documents the new dependencies.
The generic measure path (used by ELK) passed only node.dir to insertNode,
which is set for groups alone, so fork/join bars in LR diagrams were drawn
horizontal and edges hit the short side off-center. Resolve the effective
direction per node — its own dir, the nearest ancestor group's dir, or the
diagram direction — mirroring what dagre's recursive render provides via
rankdir. Also treat RL like LR in forkJoin so the bar stays perpendicular
to a horizontal flow.
…cluster backgrounds

The ER renderer carried an ELK-specific workaround from the old external ELK
renderer: svg.select('.edges').lower(), added because that renderer painted
edges above nodes and the crow's-foot markers showed on top of entity boxes.

The unified ELK render path already inserts groups in the order
clusters < edges < edgeLabels < nodes, so nodes cover marker overlap without
any reordering — and the .lower() call now moved the edge group below the
clusters group, hiding relationship edges behind subgraph backgrounds
(fully hidden with a solid background, peeking through the hachure gaps in
the hand-drawn look).

Remove the call and leave a comment explaining why it must not return.
Verified on the tmp-v12.0.0-faulty-diags ER fixtures (solid + handDrawn),
plain ER diagrams under ELK (markers still tucked under entity boxes),
nested flowchart subgraphs, edge-to-subgraph, and composite states under ELK.
An edge that terminates on a compound node ([*] --> Composite in state
diagrams, node --> subgraph in flowcharts) is anchored by ELK on the
frame's border. evenGroupFrames could pull the drawn frame in past that
anchor, leaving it floating outside the frame; the on-border check in
sanitizeElkEdgePoints then missed it and cutter2 re-clipped the edge
along a ray to the group's centre, painting a long shallow diagonal that
hugged the cluster border instead of entering it cleanly.

Feed the terminal point of every group-attached edge into the frame
pass's lane measurement so a frame is never pulled past its own edge
anchors. Only the point on the group is added - the edge's other points
belong to the layout outside the frame and must not hold it open.

Adds e2e/platform/dev-diagrams/tmp-v12.0.0-faulty-diags/
elk-legacy-preset-regression.mmd as a manual regression fixture for the
legacy-preset (Miro-matched) ELK configuration; verified pixel-identical
before and after (104/738560 px).
…-routing

fix(elk): edge from start node hugs the composite state border
…om:mermaid-js/mermaid into claude/elk-default-flowchart-layout-4d2342
A composite state or subgraph containing a loop opened on the wrong node. The
start circle shared the first layer with an ordinary state instead of having it
to itself, and dagre laid the same diagram out correctly, which is what made it
look like an ELK bug rather than a configuration gap.

It was a configuration gap. `createRootElkGraph` resolves
`elk.layered.cycleBreaking.strategy` from the preset;
`buildSubgraphLayoutOptions` never set it at all, so a container laid out on its
own fell back to ELK's default, GREEDY. The root and its own containers then
reversed different edges of the same cycle. Greedy reverses the edge that turns
an ordinary state into a source, and sources sit on the first layer — hence the
start circle sharing it.

The gap was invisible from config: every value of `elk.preset` and
`elk.cycleBreakingStrategy` rendered such a diagram identically, because none of
them reached the container. That is also how this was diagnosed — running the
root with GREEDY reproduced the container's layout byte for byte.

Containers now resolve it exactly as the root does, and the preset is hoisted
above the literal so the two functions read alike; they drifted apart in the
first place because they did not.

Only containers that contain a cycle move — an acyclic subgraph gives a
cycle-breaking strategy nothing to do. Verified unchanged on a flat state
machine, an acyclic subgraph and the nested-subgraph fixture. `preset: legacy`
still reaches containers with GREEDY, so it keeps reproducing the earlier
rendering inside frames as well as outside them.

The three new unit cases pin the two sides agreeing rather than pinning each
side's value separately, which is what allowed the drift.
…-layout-4d2342

feat(layout)!: bundle ELK and make it the default layout
@pull pull Bot locked and limited conversation to collaborators Sep 3, 2026
@pull pull Bot added the ⤵️ pull label Sep 3, 2026
@pull
pull Bot merged commit fe0e237 into code:develop Sep 3, 2026
12 of 13 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants