Skip to content

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io#17

Merged
sraphaz merged 5 commits intomainfrom
feat/drawio-export-pr16
Feb 14, 2026
Merged

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io#17
sraphaz merged 5 commits intomainfrom
feat/drawio-export-pr16

Conversation

@sraphaz
Copy link
Copy Markdown
Owner

@sraphaz sraphaz commented Feb 14, 2026

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io

Summary

This PR adds export of LikeC4 views to Draw.io (.drawio) format. Users can export from the CLI (likec4 export drawio) and from the Playground (right-click on diagram → DrawIO → Export view / Export all). This allows editing diagrams in Draw.io and reusing them in tools that support the format.

This PR does not include import. Import from Draw.io will be proposed in a separate PR after this one is merged.


What's in this PR

1. Generators (@likec4/generators)

  • packages/generators/src/drawio/generate-drawio.ts — Exports a single view or multiple views to Draw.io XML. Maps LikeC4 elements (title, description, shape, color, relationships, etc.) to mxCell vertices/edges. Supports optional layoutOverride, strokeColorByNodeId, strokeWidthByNodeId, edgeWaypoints, and compressed.
  • packages/generators/src/drawio/parse-drawio.ts — Used only for round-trip comment parsing (parseDrawioRoundtripComments): when re-exporting after a future import, layout and waypoints from comment blocks in .c4 source can be applied. No import UI or CLI in this PR.
  • packages/generators/src/drawio/index.ts — Public API: generateDrawio, generateDrawioMulti, GenerateDrawioOptions, plus getAllDiagrams, parseDrawioRoundtripComments, parseDrawioToLikeC4, parseDrawioToLikeC4Multi (for roundtrip and for the future import PR).
  • Tests: generate-drawio.spec.ts, parse-drawio.spec.ts; snapshots in __snapshots__/.

2. CLI (@likec4/likec4)

  • packages/likec4/src/cli/export/drawio/handler.tslikec4 export drawio [path] with options: --outdir, -o, --all-in-one, --roundtrip, --uncompressed, --project, --use-dot.
  • packages/likec4/src/cli/export/index.ts — Registers the drawio export command.

3. Playground

  • DrawIO context menu export only: DrawioContextMenuProvider, DrawioContextMenuDropdown (Export view…, Export all…), useDrawioContextMenuActions (handleExport, handleExportAllViews; uses generateDrawio/generateDrawioMulti and parseDrawioRoundtripComments). DrawioContextMenu, drawio-events (DRAWIO_EXPORT_EVENT). No Import item or file input in this PR.
  • Monaco: only Export to DrawIO action in editor context menu (no Import action). Integration in LanguageClientSync and routes as needed for export.

Playground exports are uncompressed by default so files open reliably in Draw.io desktop.

4. Documentation

  • apps/docs/src/content/docs/tooling/drawio.mdx — Export only: mapping (LikeC4 → Draw.io), options, not preserved, multi-diagram, troubleshooting, re-export using comment blocks. No import sections.
  • apps/docs/src/content/docs/tooling/cli.mdx — Export to DrawIO section only; no Import from DrawIO section. Intro mentions Export to DrawIO only.

5. Tests

  • packages/likec4/src/drawio-demo-export-import.spec.ts — Export tests only; import/vice-versa test skipped in this PR.
  • packages/likec4/src/drawio-tutorial-export-import.spec.ts — Export tests only; import and round-trip tests skipped in this PR.
  • e2e/tests/drawio-playground.spec.ts — Asserts DrawIO menu shows Export to DrawIO (and Export all). No Import assertion. Run with playwright.playground.config.ts (playground on 5174); main e2e config ignores this test.

What's not in this PR

  • No likec4 import drawio command (no packages/likec4/src/cli/import/).
  • No Playground "Import from DrawIO" menu item, file input, or Monaco Import action.
  • No docs for importing from Draw.io.
  • Import/round-trip tests in likec4 specs are skipped; enabled in the import PR.

Post-review fixes (CodeRabbit)

Addresses CodeRabbit AI review (actionable + nitpicks):

Actionable

  • parse-drawio.ts: stripHtml now uses shared decodeXmlEntities() (covers ' and all five XML entities). UserObject id in constructed mxCell tag is escaped with escapeXml(userObjId). collectRoundtripForState consolidated to a single pass over idToFqn where possible.
  • drawio handler: Explicit guard when --all-in-one and zero views: warn and throw ERR_NO_VIEWS_EXPORTED. ensureSingleProject() only called when no --project is provided, so multi-project workspaces can use --project.
  • json handler: languageServices created with await using so it is disposed on exit (file watchers/RPC cleaned up).
  • png handler: startTakeScreenshot moved inside the per-project loop so timing reported is per-project, not cumulative.

Nitpicks / cleanup

  • useWorkspaceId.ts: Removed commented-out code and unused extensionContext import.
  • documentation-provider.ts: Added exhaustiveness check (const _exhaustive: never = node) so new node types require a handler.
  • log: configureLogtape now uses explicit generics instead of any; typo fix gerErrorFromLogRecordgetErrorFromLogRecord.
  • DrawioContextMenuProvider: Typed EMPTY_DRAWIO_SNAPSHOT; stable getSourceContent via ref to avoid churn on every snapshot.
  • useDrawioContextMenuActions: Download uses detached anchor (no appendChild/removeChild); dev-only console.warn when a view is skipped in fillFromModelView.
  • generate-drawio.ts: Optional modified?: string in GenerateDrawioOptions and wrapInMxFile for deterministic output (tests/caching).

Second batch (CodeRabbit follow-up)

  • parse-drawio.ts: getDecodedStyle and decodeRootStyleField wrap decodeURIComponent in try/catch so malformed percent-encoding (e.g. %ZZ) does not abort parsing; on error return raw string (or undefined/empty). Ternary titlePart clarified with parentheses: (desc || tech) ? ....
  • log (index.ts): Destructure config to exclude sinks/loggers from spread (...restConfig) so overrides are not misleading.
  • log (formatters.ts): Typo errorMessgeerrorMessage in appendErrorToMessage; removed commented-out line.
  • DrawioContextMenuProvider: filesRef.current = files moved into useEffect([files]) for concurrent-safety.
  • drawio handler: exportParams explicitly typed as ExportDrawioParams; single zero-views guard for both all-in-one and per-view modes.
  • png handler: Logger prefix aligned with json: createLikeC4Logger('c4:export').
  • generate-drawio.ts: Helper buildOptionsFromRoundtrip(viewId, roundtrip, overrides) extracted; buildDrawioExportOptionsForViews parses source once and distributes options (parse-once optimization for many views).

Checklist (contribution guidelines)

  • I have read the latest contribution guidelines.
  • I have rebased my branch onto main before creating this PR.
  • My commit messages follow Conventional Commits (e.g. feat:, docs:).
  • I have added/updated tests for export; import tests are skipped in this branch.
  • I have run pnpm test and pnpm typecheck (and pnpm test:e2e for e2e); all pass.
  • Documentation updated (drawio.mdx and cli.mdx for export only).
  • A changeset has been added for user-facing packages if applicable.

Notes for reviewers

  • Export: one .drawio file per view by default; --all-in-one for all views as tabs; --roundtrip applies layout/waypoints from comment blocks; --uncompressed for Draw.io desktop compatibility.
  • Playground exposes only Export actions in the DrawIO menu.
  • Generators: parse-drawio is present for round-trip comments and for the upcoming import PR; no import entrypoints used here.
  • E2E: drawio-playground test is excluded from main config (runs only with playground config on port 5174); screenshot diff tolerance and timeouts tuned for CI.

Review context

The original DrawIO bidirectional work (branch feat/drawio-bidirectional-playground) was reviewed upstream in likec4/likec4 PR #2593Fix DrawIO CLI docs, refactor context menu, correct XML generation. Review was done by @sraphaz at the request of @davydkov. This export-only PR is a split from that work; feedback from that review has been incorporated where applicable (e.g. CLI docs, context menu structure, XML generation). CodeRabbit AI review comments have been addressed in the “Post-review fixes” section above.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Stabilizes Draw.io UI callbacks via a ref-based file cache, threads an optional ISO modified timestamp through drawio generation, consolidates XML parsing into a single-pass collector, adds a compile-time exhaustiveness guard, tightens CLI guards/resource disposal, and renames/centralizes some logging and workspace helpers.

Changes

Cohort / File(s) Summary
Draw.io UI & Actions
apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx, apps/playground/src/components/drawio/useDrawioContextMenuActions.ts
Add useRef-backed filesRef and memoize getSourceContent; avoid attaching download anchor to DOM; log DEV warning when view resolution fails.
Draw.io Generation
packages/generators/src/drawio/generate-drawio.ts
Add GenerateDrawioOptions.modified?: string; thread optional modified through wrapInMxFile, generateDrawio, and multi-diagram generation to control mxfile modified attribute.
Draw.io Parsing
packages/generators/src/drawio/parse-drawio.ts
Use escapeXml/decodeXmlEntities; refactor roundtrip collection into a single-pass that builds layout, stroke color/width, custom data, and waypoint lines with guarded edge handling.
CLI Export Handlers
packages/likec4/src/cli/export/drawio/handler.ts, packages/likec4/src/cli/export/json/handler.ts, packages/likec4/src/cli/export/png/handler.ts
Only enforce single-project when no explicit project provided; guard and error on zero-view exports; use await using for resource disposal; switch PNG timing to per-project measurement.
Type Safety (Language Server)
packages/language-server/src/documentation/documentation-provider.ts
Add compile-time exhaustiveness guard in getDocumentation to ensure all AstNode variants are handled.
Logging & VSCode helper
packages/log/src/formatters.ts, packages/log/src/index.ts, packages/vscode/src/useWorkspaceId.ts
Rename gerErrorFromLogRecordgetErrorFromLogRecord; centralize sinks map to include console sink with strong typing; remove unused workspace-state persistence code.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I cached the files with gentle hops,
One-pass parsing tidy as carrot crops,
Timestamps tucked in each mxfile sheet,
CLI checks hum, downloads neat,
Hop — commit — nibble on new feats! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (11 files):

⚔️ apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx (content)
⚔️ apps/playground/src/components/drawio/useDrawioContextMenuActions.ts (content)
⚔️ packages/generators/src/drawio/generate-drawio.ts (content)
⚔️ packages/generators/src/drawio/parse-drawio.ts (content)
⚔️ packages/language-server/src/documentation/documentation-provider.ts (content)
⚔️ packages/likec4/src/cli/export/drawio/handler.ts (content)
⚔️ packages/likec4/src/cli/export/json/handler.ts (content)
⚔️ packages/likec4/src/cli/export/png/handler.ts (content)
⚔️ packages/log/src/formatters.ts (content)
⚔️ packages/log/src/index.ts (content)
⚔️ packages/vscode/src/useWorkspaceId.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding export functionality for LikeC4 views to Draw.io format across CLI, Playground, generators, and docs packages.
Description check ✅ Passed The description is comprehensive and well-structured, covering all major changes, what's included/excluded, post-review fixes, tests, and a detailed checklist addressing all required template items.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/drawio-export-pr16
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/drawio-export-pr16
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/likec4/src/cli/export/drawio/handler.ts (1)

268-285: ⚠️ Potential issue | 🟡 Minor

Inconsistent zero-view handling between all-in-one and per-view modes.

The new guard at lines 268-271 correctly warns and throws when all-in-one mode has zero views. However, the per-view mode (else branch) will silently succeed when viewmodels.length === 0:

  • succeeded will be 0
  • The condition succeeded === 0 && viewmodels.length > 0 evaluates to false (since 0 > 0 is false)
  • No warning or error is logged

For consistency, consider adding the same guard for per-view mode:

Proposed fix
   if (args.allInOne && viewmodels.length === 0) {
     logger.warn('No views to export in all-in-one mode')
     throw new Error(ERR_NO_VIEWS_EXPORTED)
   }
   if (args.allInOne && viewmodels.length > 0) {
     try {
       await exportDrawioAllInOne(exportParams)
     } catch (err) {
       logAndRethrow(logger, ERR_EXPORT_FAILED, err)
     }
   } else {
+    if (viewmodels.length === 0) {
+      logger.warn('No views to export')
+      throw new Error(ERR_NO_VIEWS_EXPORTED)
+    }
     const { succeeded } = await exportDrawioPerView(exportParams)
     if (succeeded === 0 && viewmodels.length > 0) {
       logger.error(ERR_NO_VIEWS_EXPORTED)
       throw new Error(ERR_NO_VIEWS_EXPORTED)
     }
     if (succeeded > 0) logger.info(`${k.dim('total')} ${succeeded} DrawIO file(s)`)
   }
🧹 Nitpick comments (2)
packages/generators/src/drawio/generate-drawio.ts (1)

1274-1281: Consider accepting modified as a top-level parameter for clearer API.

The current implementation extracts modified from the first view's options, which works correctly since mxfile has a single modified attribute. However, this behavior may be surprising if a caller sets different modified values on different views.

For clearer API semantics, consider accepting modified as a separate parameter:

♻️ Proposed refactor for API clarity
 export function generateDrawioMulti(
   viewmodels: Array<DrawioViewModelLike>,
   optionsByViewId?: Record<string, GenerateDrawioOptions>,
+  modified?: string,
 ): string {
   const diagrams = viewmodels.map(vm => generateDiagramContent(vm, optionsByViewId?.[vm.$view.id]))
-  const modified = viewmodels.length > 0 ? optionsByViewId?.[viewmodels[0]!.$view.id]?.modified : undefined
   return wrapInMxFile(diagrams, modified)
 }

Alternatively, document the current behavior in the JSDoc to set expectations.

apps/playground/src/components/drawio/useDrawioContextMenuActions.ts (1)

154-156: Include the caught error in the DEV warning.

Right now the warning only prints the viewId, which hides the underlying error when it’s not a “missing view” case. Logging the error will make diagnosis easier.

🛠️ Suggested tweak
-      } catch {
-        // view might not exist for this id — ignore gracefully
-        if (import.meta.env.DEV) console.warn(`fillFromModelView: skipped viewId=${viewId}`)
+      } catch (err) {
+        // view might not exist for this id — ignore gracefully
+        if (import.meta.env.DEV) console.warn(`fillFromModelView: skipped viewId=${viewId}`, err)
       }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant