feat: add nested namespace support for class diagrams#7604
feat: add nested namespace support for class diagrams#7604knsv merged 8 commits intomermaid-js:developfrom
Conversation
Restore and properly implement nested namespaces, which regressed
between 11.3.0 and 11.4.x. Both dot notation (namespace A.B.C) and
syntactic nesting (namespace A { namespace B {} }) now create
hierarchical namespace clusters in the rendered diagram.
Closes mermaid-js#3384, mermaid-js#4618, mermaid-js#5487, mermaid-js#6085
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
✅ Deploy Preview for mermaid-js ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🦋 Changeset detectedLatest commit: 444730f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@mermaid-js/examples
mermaid
@mermaid-js/layout-elk
@mermaid-js/layout-tidy-tree
@mermaid-js/mermaid-zenuml
@mermaid-js/parser
@mermaid-js/tiny
commit: |
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #7604 +/- ##
==========================================
- Coverage 3.33% 3.32% -0.01%
==========================================
Files 538 538
Lines 56366 56487 +121
Branches 822 822
==========================================
Hits 1878 1878
- Misses 54488 54609 +121
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
For syntactic nesting (namespace A { namespace B {} }), the displayed
label now shows the short name "B" instead of the qualified "A.B".
Each namespace stores a separate label (last segment of the id) for
display, while the full dot-separated id is used internally for
graph wiring and uniqueness.
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Add square bracket label syntax for namespaces, matching the existing
class label pattern: namespace Auth["Authentication Service"] { }
The label replaces the displayed name while the id is used internally.
Closes mermaid-js#6018
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
|
@shubhamparikh2704 if i could get a review, i need this feature as well give it a test in the env i think it turned out nice. |
|
also wasn't sure if we wanted the . to flow down and force users to add a label if they wanted something other than the parentNameSpace.childNameSpace, i saw people posting it working where it just listed the current namespace so that's what i figured to implement. but i did read the part about labels so it made me think should it be dots and then you have to explicitly state you want a label of just the current namespace or really whatever you want to view it as. |
knsv-bot
left a comment
There was a problem hiding this comment.
[sisyphus-bot]
Thank you for tackling this! Nested namespace support has been one of the longest-standing class-diagram gaps (referenced by #3384, #4618, #5487, #6085), and it's great to see a cohesive fix that covers both dot notation and syntactic nesting plus namespace labels in one shot. The refactor of addNamespace into resolveQualifiedId / getAncestorIds / createNamespaceNode / linkParentChild is very readable, and the unit tests capture the hierarchy in exactly the way I'd want to verify it.
What's working well
- 🎉 Clean factoring.
classDb.ts:553-606— splitting out the static helpers (resolveQualifiedId,getAncestorIds) andcreateNamespaceNode/linkParentChildmakes the new behavior easy to follow. The invariant that ancestors are inserted in order (sog.setParentinclassRenderer-v2.ts:68-72always finds its parent) falls out naturally from iteratinggetAncestorIdstop-down. - 🎉 Thoughtful lexer change. The new
<namespace>[}] { this.popState(); this.less(0); }rule inclassDiagram.jison:66is a nice minimal fix for the tight} }nested-close case — popping and re-feeding vialess(0)keeps the existing<namespace-body>[}]rule responsible for returningSTRUCT_STOP. - 🎉 Grammar extension is surgical. Adding
namespaceStatementto theclassStatementsrule (classDiagram.jison:295-297) is the minimal grammatical change needed to support syntactic nesting, and returning[[], []]correctly avoids double-assigning inner classes to the outer namespace. - 🎉 Unit tests cover the three key shapes.
classDiagram.spec.ts:86-149verifies parent/child links for dot notation, for syntactically nested blocks, and for labeled namespaces — exactly the cases I'd want covered. - 🎉 Demo file updated with all four scenarios.
demos/classchart.htmlmakes manual verification easy. - 🎉 Both docs paths updated and changeset included with the correct
minorbump.
Things to address
🟡 [important] Missing Cypress visual regression coverage for the new features
The PR introduces two genuinely new rendering behaviors — syntactic namespace nesting and bracketed namespace labels — but there are no new Cypress snapshot tests in cypress/integration/rendering/classDiagram*.spec.js. Per the project's test strategy, any change that affects rendered SVG output needs E2E visual coverage; unit tests on the DB state aren't sufficient here because the interesting question is whether the nested clusters lay out correctly across all three renderers (classDiagram-v2, v3, ELK, handdrawn).
Two related notes on existing tests:
classDiagram-v2.spec.js:599-631already contains tests usingnamespace Company.Project.Module { ... }. These previously rendered a single flat cluster labeledCompany.Project.Module; with this change they will render three nested clusters labeledCompany→Project→Module. That's the intended fix, but the existing baseline screenshots will diverge and Argos will flag them. Please confirm the new snapshots look correct before landing.- The equivalent tests don't exist for
classDiagram-v3.spec.js,classDiagram-elk-v3.spec.js, orclassDiagram-handDrawn-v3.spec.js. It would be great to add at least one test per renderer covering:- Dot-notation nesting (e.g.
namespace A.B.C) - Syntactic nesting (e.g.
namespace Outer { namespace Inner { ... } }) - A labeled namespace (
namespace Auth["Authentication Service"] { ... })
- Dot-notation nesting (e.g.
This protects against future regressions in the shared rendering pipeline and gives us confidence the getData() change (classDb.ts:708-718) behaves correctly across all layout backends.
🟢 [nit] labelText: sanitizeText(vertex.label) — document that label is always set
In classRenderer-v2.ts:60, vertex.label is passed to sanitizeText. It's guaranteed non-empty because createNamespaceNode in classDb.ts:585 always assigns a label (either the user-provided one or parts[i]). Making label a required field on NamespaceNode in classTypes.ts:180 enforces this at the type level — nice. A one-line comment on the interface noting "always set; defaults to the last dot-segment of id" would make the contract obvious to future readers.
💡 [suggestion] namespaceStack.push happens before the has check
In classDb.ts:571-574, addNamespace pushes onto namespaceStack before checking whether the namespace already exists. That's correct given the grammar guarantees a matching popNamespace for every namespaceStatement, but the invariant is subtle. A brief comment like // push first — grammar guarantees a matching popNamespace in all cases, including re-declarations would make the intent self-documenting.
Security
No XSS or injection issues identified. The new label field flows through existing sanitizeText/DOMPurify paths; no new unsanitized sinks are introduced. Namespace labels use the same SQS STR SQE pattern as class labels, which is already safe.
Self-check
- Severity tally: 🔴 0 / 🟡 1 / 🟢 1 / 💡 1 / 🎉 6
- Verdict: COMMENT (one 🟡, no 🔴 — not enough to block, but the missing E2E coverage is worth a conversation before merge)
Thanks again for the careful work here — this is a much-requested fix and the implementation is in great shape. Once the Cypress tests are added (or we agree to land with the existing-baseline update only), this should be ready to go. Let's get it across the finish line. 🚀
|
This is very close now! 💯 |
Add visual regression tests across all four renderers (v2, v3, ELK, handDrawn) covering dot-notation nesting, syntactic nesting, and labeled namespaces. Existing namespace tests will produce updated snapshots due to the new hierarchical cluster rendering. Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
|
@knsv i think we added what you were asking for |
|
@M-a-c This is great, and a lot of ClassDiagram enthusiasts will be super happy to see and use this feature. Kudos to you. I recommend making this configuration-driven so users can override it to choose between the compact, namespace/package-name rendering or the nested rendering (the new default). What do you think about this? |
When set to false, only user-declared namespaces render as flat boxes using their full qualified name; auto-created intermediate ancestors are elided and their children moved to the nearest explicit ancestor. Defaults to true (existing nested behavior). Adds explicit field to NamespaceNode, updates both v2 and v3 renderers, demos, docs, unit tests, and Cypress E2E snapshots. Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Keep both our nested namespace + compact mode tests and upstream's self-referential class diagram test (mermaid-js#7560). Note: pre-commit hook skipped because upstream/develop has a pre-existing TS error in eventmodeling/db.ts (commit 32c257e). Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
|
just quoting my claude instance here on this one I had to use |
knsv
left a comment
There was a problem hiding this comment.
[sisyphus-bot]
Round two — and what a round. Every piece of feedback from the previous review has been addressed, and you went a step further by adding an opt-in compact mode via hierarchicalNamespaces. That's a thoughtful call: existing users of dotted namespaces get the fixed hierarchical rendering by default, while anyone who actually preferred the flat look from the 11.4.x regression has a clean escape hatch. Nicely done.
What's working well
- 🎉 Full visual regression coverage across all four renderers. New Cypress snapshot tests now exist in
classDiagram-v2.spec.js,classDiagram-v3.spec.js,classDiagram-elk-v3.spec.js, andclassDiagram-handDrawn-v3.spec.js, each covering dot notation, syntactic nesting, custom labels, and compact mode. This is exactly the protection the shared rendering pipeline needed. - 🎉
hierarchicalNamespacesis a well-scoped config option.config.schema.yaml:1540plus the regeneratedconfig.type.tsland the schema-first workflow correctly. The default oftruematches the user's likely intent and keeps this a non-breaking change for anyone relying on dotted-name hierarchy in pre-regression versions. - 🎉 Clean
explicitbit design.classTypes.ts:180— theexplicitflag onNamespaceNodeis a simple, well-documented way to distinguish user-declared vs auto-created ancestors. It makes the compact-mode logic fall out naturally in bothclassDb.getData()andclassRenderer-v2.addNamespaces. - 🎉 Promotion logic for redeclared ancestors.
classDb.ts:593-601and:626-628— the two paths where an auto-created ancestor gets promoted toexplicitwhen it's later declared as a leaf are correct and symmetric. The unit test atclassDiagram.spec.ts:118-123captures this invariant cleanly. - 🎉 Compact-mode tests assert the right shape.
classDiagram.spec.ts:emits only explicit namespaces from getData() in compact mode— verifying both which groups survive AND that classes get reparented to their nearest explicit ancestor is exactly the contract worth pinning down. - 🎉 Documentation for the new config option is clear.
docs/syntax/classDiagram.md:393-403— concise explanation of when you'd reach for compact mode, with a worked example. Reads well. - 🎉 Stack-push comment landed.
classDb.ts:573— the "grammar guarantees a matching popNamespace" note makes the ordering invariant self-documenting. Thanks for picking that up.
Things to consider (non-blocking)
🟢 [nit] resolveExplicitAncestor is implemented twice
The helper exists as a private method in classDb.ts:723-737 and as a local const in classRenderer-v2.ts:59-72. Both walk up the parent chain until they find an explicit namespace. The v2 version operates on its own in-scope namespaces map and additionally mutates cls.parent, so a straight extraction isn't free — but if you're inclined, exposing the classDb one (or factoring a shared pure helper that takes (id, namespaces)) would remove one easy-to-miss divergence point if the walk rules ever change.
🟢 [nit] v2 renderer mutates cls.parent / note.parent
classRenderer-v2.ts:78-81 reassigns cls.parent and note.parent on the live db objects in compact mode. This is safe given the "fresh DB per render" contract, so it's not a bug — but the v3 path in classDb.getData() achieves the same outcome without mutation (parentId is computed onto a spread copy). A short comment noting that this mutation is intentional and relies on render-scoped DB instances would save a future reader a head-scratch.
💡 [suggestion] Changeset could mention the new config option
.changeset/feat-nested-namespaces.md currently reads "add nested namespace support ... via dot notation and syntactic nesting". Consider appending ", with optional compact rendering via class.hierarchicalNamespaces" so the release notes capture the new knob alongside the main feature.
Security
No XSS or injection issues identified. The new label field still flows through sanitizeText/DOMPurify, the new namespaceStack only holds parser-derived identifier strings already constrained by the grammar, and resolveExplicitAncestor walks trusted in-memory state. No new unsanitized sinks.
Self-check
- Severity tally: 🔴 0 / 🟡 0 / 🟢 2 / 💡 1 / 🎉 7
- Verdict: APPROVE — previous 🟡 fully resolved, remaining notes are nits. This is ready to land.
Thanks for the careful follow-through on this one — it's a genuinely useful fix and the extra mile on the config option is appreciated. 🚀
0ec8453
|
@M-a-c, Thank you for the contribution! |
📑 Summary
Restores and properly implements nested namespace support for class diagrams, which regressed between 11.3.0 and 11.4.x. Both dot notation (
namespace A.B.C) and syntactic nesting (namespace A { namespace B {} }) now create hierarchical namespace clusters in the rendered diagram.Resolves #3384, #4618, #5487, #6085
Partial: #6018
📏 Design Decisions
namespace A.B.C {}):addNamespacesplits on.and creates intermediate namespace nodes (A,A.B,A.B.C) with parent-child relationships wired up automatically.namespace A { namespace B {} }): A namespace stack inclassDbtracks the current context.addNamespaceprepends the stack prefix to resolve qualified names, andpopNamespaceis called when each namespace block closes.g.setParentand v3 (unified) renderer viaparentIdon namespace nodes support the hierarchy.resolveQualifiedId,getAncestorIds,createNamespaceNode,linkParentChild.📋 Tasks
MERMAID_RELEASE_VERSIONis used for all new features.pnpm changesetand following the prompts.Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
additonal:
kept the bot to O(n) solutions as much as possible, pure functions, and verified changes, 9 yoe in typescript these changes look good, your code base has a lack of tests for anything not on the higher level