LearnNewsExamplesServices
Frontmatter
id18564
titleMonaco''s AMD loader swallows mermaid''s UMD define, so no diagram renders
stateClosed
labels
bugairegressionportal
assigneesneo-opus-ada
createdAtSep 9, 2026, 9:30 PM
updatedAtSep 9, 2026, 11:23 PM
githubUrlhttps://github.com/neomjs/neo/issues/18564
authorneo-opus-ada
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 9, 2026, 11:23 PM

Monaco's AMD loader swallows mermaid's UMD define, so no diagram renders

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

Context

@tobiu reported it with the diagnosis attached, which is why this is a leaf rather than an investigation:

loader.js:965 Uncaught Error: Can only have one anonymous define call per script file
    at c.enqueueDefineAnonymousModule (loader.js:965:11)
    at p (loader.js:1297:48)
    at mermaid.min.js:456:3421
Mermaid.mjs:66 Uncaught (in promise) ReferenceError: mermaid is not defined
    at Mermaid.loadFiles (Mermaid.mjs:66:9)

loader.js is Monaco's AMD loader, and it is in the stack calling into mermaid's define. Split off #18548, which owns the broader "a guide's diagram can be render-verified before merge" path and whose ACs span this repo and neo-agent-skills. This is the proximate defect underneath it.

The Problem

main.addon.Mermaid loads node_modules/mermaid/dist/mermaid.min.js — the UMD bundle. UMD checks for define.amd before falling back to a global. main.addon.MonacoEditor:129 loads Monaco's loader.js, which publishes exactly that define.

On any page carrying both, mermaid's anonymous define lands in Monaco's loader, which refuses it (Can only have one anonymous define call per script file). The library then never attaches to window, and loadFiles throws mermaid is not defined on the next line. Every diagram on the page fails.

Measured on the running portal, dev @ bf4a73ce4b:

probe result
!!(window.define && window.define.amd) on the guide route true — the loader is present
.neo-mermaid containers 4
.neo-mermaid svg 0

The portal is the surface where both libraries meet, which is why the symptom reads as portal-only.

Regressed by ffd860b664 (2026-09-05, "fix(monaco): restore editor initialization and lifecycle (#5822) (#18355)"), which restructured loadFiles and its loader install. The Mermaid addon has not changed since January — the collision arrived from the other side.

The Architectural Reality

  • src/main/addon/Mermaid.mjs — mermaidPath points at mermaid.min.js (UMD); loadFiles uses DomAccess.loadScript; loadFiles and render both reach a bare global mermaid.
  • src/main/addon/MonacoEditor.mjs:129 — await DomAccess.loadScript(path + '/loader.js').
  • node_modules/mermaid/dist/ ships mermaid.esm.min.mjs, and the package's own exports map resolves . to an ESM entry. The UMD bundle is the one build we should not be using.
  • src/util/HighlightJs.mjs:104 is the in-repo precedent for importing a library as a module: await import(/* webpackIgnore: true */ path).

The Fix

Import the ESM build instead of script-tagging the UMD one, and hold the library on the addon instead of on window — the same shape as util.HighlightJs#hljs. An ESM import has no define to find, so the conflict cannot occur regardless of load order.

One trap this has to get right, and it is not obvious. Neo.config.basePath is written for DomAccess.loadScript, whose <script src> resolves against the document. A dynamic import() resolves against the importing module's own URL, so a bare basePath from src/main/addon/ resolves to /src/node_modules/… and 404s. The path must be resolved against document.baseURI explicitly, which also makes it depth-independent. (HighlightJs avoids this only because src/util/ happens to sit at the depth its ../../ assumes — worth knowing before that file moves.)

Acceptance Criteria

  • AC-1 — a guide diagram renders as SVG on a page that also carries Monaco's AMD loader. Asserted on the rendered SVG, never the container: the container is emitted from the fence alone and is present throughout the outage, so counting containers is precisely the assertion that would have stayed green.
  • AC-2 — the arm asserts Monaco's loader is present. Without it, a future change that stopped loading Monaco on that route would make the arm pass by removing the conflict rather than by fixing it.
  • AC-3 — red-first, against the real defect. Reverting only the addon change turns the arm red; restoring it returns green. Both directions, same arm.
  • AC-4 — the arm runs in CI. Verified against the CI-shaped selection (--list with NEO_AGENTOS_RUNTIME_ROOT unset), not merely against a local run — most of the e2e tier does not run there.
  • AC-5 — no global mermaid. The library is held on the addon; git grep finds no bare-global read left in the file. A UMD global is a name any other script on the page can collide with, which is the class of defect being removed.

Out of Scope

  • #18548's verification path — a documented, author-runnable render check with a deliberate-syntax-error control, plus the guide-authoring §3 corrections. Those ACs span neo-agent-skills; this ticket unblocks them by making the renderer work, and does not claim them.
  • Monaco's AMD loader itself. It is global and will collide with the next UMD script someone loads. Removing that hazard means changing how Monaco is loaded, which is a far larger change with its own risk; recorded here as the residual it is.
  • Mermaid's reachability in a dist build. dist/ contains no mermaid and no build script copies it, so the node_modules path is a live question for a real deployment — separate from this dev-mode regression, and not made worse by it.
  • The portal instantiating guide content twice. Measured in passing: 4 containers for 2 fences, in two identical pairs. Pre-existing, unrelated, noted so the next reader of that count is not surprised.

Avoided Traps

  • Asserting the container exists. #18548 names this explicitly: the container proves the fence parsed, not that anything rendered.
  • Concluding from a hidden pane. Mermaid renders on layout, so an unlaid-out browser pane produces zero SVGs for a perfectly good fence — the reason the measurement above records a visible, sized viewport.
  • Keeping the UMD bundle and stashing window.define around the load. It works, and it leaves the page with a global mermaid plus a load-order dependency that the next maintainer has to know about.

Decision Record impact

none.

Related

  • #18548 — the verification-path ticket this splits from and unblocks
  • #18355 / ffd860b664 — the Monaco lifecycle change that introduced the collision
  • #18354 — Monaco bundling, adjacent

Handoff Retrieval Hints

  • query_raw_memories: "mermaid UMD monaco AMD loader anonymous define portal diagram"
  • Reproduce: portal route #/learn/guides/uibuildingblocks/DockLayouts (slashes, not dots — a dotted route silently renders an empty pane), then evaluate document.querySelectorAll('.neo-mermaid svg').length
  • Anchors: src/main/addon/Mermaid.mjs, src/main/addon/MonacoEditor.mjs:129, src/util/HighlightJs.mjs:104

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