LearnNewsExamplesServices
Frontmatter
id18568
titleMermaid's lazy chunks still register into Monaco's loader at first render
stateClosed
labels
bugairegressionbuildportal
assigneesneo-opus-ada
createdAtSep 9, 2026, 11:23 PM
updatedAtSep 10, 2026, 12:34 AM
githubUrlhttps://github.com/neomjs/neo/issues/18568
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 10, 2026, 12:34 AM

Mermaid's lazy chunks still register into Monaco's loader at first render

Closed v13.2.0/archive-v13-2-0-chunk-20 bugairegressionbuildportal
neo-opus-ada
neo-opus-ada commented on Sep 9, 2026, 11:23 PM

Context

Successor to #18564, which #18565 resolved and which must stay closed — a ticket resolved by a merged PR is a record, and reopening it makes the Resolves linkage ambiguous. #18564's fix was correct and incomplete: it removed the collision at library load and left it at first render.

@neo-opus-grace found the remainder on merged dev and I reproduced it independently before accepting.

route: learn/tutorials/DockLayoutsFirstLayout   at dev@87e3786673
containers: 2 | svg: 0 | define.amd: true | livePreviews: 2
[error] Can only have one anonymous define call per script file
        at …/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs:6:203

The Problem

mermaid.esm.min.mjs is a 30 KB entry that lazily imports from dist/chunks/mermaid.esm.min/ — 206 chunks. The entry carries no define, which is why #18565's switch made loadFiles() and initialize() succeed. Ten chunks still do, so the anonymous define lands in Monaco's loader at the moment a diagram first renders.

measure value
.mjs files under mermaid/dist 274
files containing typeof define 10
define( call sites 26
of those, guarded by define.amd 22

⚠️ The .amd guard does not protect us, and that is the counter-intuitive part. Monaco's define has .amd, so typeof define=="function"&&define.amd takes the AMD branch exactly like the unguarded wrappers do. All 26 sites are hazards under Monaco. Two of them are vendored fastdom, not mermaid's own code, and check only typeof define == "function".

Why #18565's arm went green. LearnMermaidRender.spec.mjs drives guides/uibuildingblocks/DockLayouts, which has zero live-preview fences. @neo-opus-grace's diagnosis is the general form and it corrects that arm's AC-2 directly:

The precondition asserts the loader is present. The defect needs the loader to be busy.

define.amd === true on both routes, because the portal installs the loader app-wide — so the precondition passes everywhere and separates nothing. Monaco only has an anonymous define in flight where live previews instantiate editors.

route mermaid fences live-preview fences define.amd with SVG
guides/uibuildingblocks/DockLayouts 2 0 true 4/4 ✅
tutorials/DockLayoutsFirstLayout 1 3 true 0/2 ❌

Same session, same tab, failing route re-tested immediately after the passing one — so not a poisoned loader, not a cache, not the diagram type.

The Architectural Reality

Two approaches are already falsified, before anyone spends a cycle on them:

  • Hiding window.define around the render. Chunks load asynchronously and, on exactly the routes that break, Monaco is concurrently loading modules. Hiding define trades a mermaid failure for a Monaco one.
  • Rewriting the identifier (define → an undefined name). It breaks the typeof define && define.amd form: typeof X yields the truthy string "undefined", and the right-hand X.amd then throws a ReferenceError. A rewrite that neutralises one guard form detonates another.

What works — verified end to end, not reasoned. A module-scope shadow prepended to each affected chunk:

const define={amd:!1};
guard form present in dist with the shadow
typeof define=="function" "object"=="function" → false
typeof define && define.amd "object" && false → false
(typeof define)[0]=="f" "o"=="f" → false

Every form takes the non-AMD branch and none throws. No affected file declares define already (grep -c 'const define\|let define\|var define\|function define' → 0), and an ES module's top-level binding is file-scoped, so the shadow cannot leak.

Proof, patching one chunk locally on the failing route and reverting byte-identical afterwards:

before after
containers 2 2
with SVG 0 2
console error anonymous-define none

The Fix

A build step that copies node_modules/mermaid/dist/ into dist/mermaid/ and prepends the shadow to the affected files in transit, with main.addon.Mermaid#mermaidPath pointed at the built artifact.

It also closes a second, latent defect recorded on #18564's Out of Scope: dist/ contains no mermaid and no build script copies one, so today's node_modules path is unreachable in a real deployment. One artifact answers both.

Not the build-highlightjs shape. I proposed that precedent before reading it: buildScripts/build/highlightJs.mjs clones the upstream repository and builds from source. For mermaid's 206-chunk esbuild output that is disproportionate — copy-and-transform is the smaller correct thing, and it keeps the pinned npm version as the single source of truth.

Acceptance Criteria

  • AC-1 — a diagram renders on a route where Monaco is BUSY. Verified on a live-preview route, not merely a loader-present one. This is the whole ticket; guides/…/DockLayouts already passes today and re-proving it is not evidence.
  • AC-2 — the arm's precondition discriminates. ⚠️ define.amd === true is true on the passing and failing routes alike and separates nothing. The precondition must assert the condition the defect needs — an instantiated live preview / editor — or the arm repeats #18565's mistake with a new coordinate.
  • AC-3 — red-first against THIS defect. At dev@87e3786673 the tutorial route is red and the guide route green, so a correct arm distinguishes them before any fix lands. Both directions on the same arm.
  • AC-4 — every affected chunk is covered, not the one this reproduction happened to load. chunk-DZP67EKU.mjs sufficed for this route; ganttDiagram-*, architectureDiagram-*, cose-bilkent-* and the non-min mermaid.esm/ twins all carry wrappers, and other diagram types load them. The transform is driven by a scan, never a filename list.
  • AC-5 — the transform fails loudly on an upstream change. If a future mermaid version introduces a guard form the shadow does not satisfy, or a file that already declares define, the build refuses rather than emitting a silently broken artifact.
  • AC-6 — dist/ reachability. A diagram renders in a built environment where node_modules is not part of the deployed tree.

Out of Scope

  • #18548's author-runnable verification path — the documented render-check with a deliberate-syntax-error control, plus the guide-authoring §3 corrections that span neo-agent-skills. This unblocks it further; it does not claim it.
  • The portal instantiating guide content twice. Measured in passing on both routes — 2 containers for 1 fence, identical textContent length. Pre-existing, unrelated, and it is why container counts read double.
  • Patching node_modules at install time (postinstall, patch-package). It would fix the dev path and leave dist/ unreachable, i.e. solve the smaller half of the problem and hide the larger one.

Avoided Traps

  • Sampling instead of polling. The first measurement after the patch read svg: 1 of 2 and I nearly filed a partial fix. Mermaid renders per-container asynchronously; the settled state is 2/2. This bit me twice today on the same surface.
  • Asserting the container. #18548 names it: the container is emitted from the fence alone and is present throughout the outage.
  • Trusting a .amd guard. 22 of 26 sites have one and every one of them still fires under Monaco.

Decision Record impact

none. A build artifact and a path change; no runtime contract moves.

Related

  • #18564 / #18565 — the entry-level half, resolved and staying closed
  • #18548 — the verification-path ticket this unblocks further
  • #18355 / ffd860b664 — the Monaco lifecycle change that made the collision reachable

Handoff Retrieval Hints

  • query_raw_memories: "mermaid lazy chunk UMD define shadow monaco busy live preview"
  • Reproduce: #/learn/tutorials/DockLayoutsFirstLayout on a fresh origin, then poll document.querySelectorAll('.neo-mermaid svg').length
  • Anchors: node_modules/mermaid/dist/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs (the fastdom wrappers), src/main/addon/Mermaid.mjs#mermaidPath, buildScripts/build/highlightJs.mjs (the precedent NOT to copy)

Origin Session ID: 0dd7d644-f279-4b53-a515-522346f5f28f