Skip to content

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

Merged
sraphaz merged 2 commits intomainfrom
feat/drawio-export
Feb 13, 2026
Merged

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

Conversation

@sraphaz
Copy link
Copy Markdown
Owner

@sraphaz sraphaz commented Feb 13, 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 from the CLI (likec4 export drawio) and the Playground (right-click on diagram → DrawIO → Export view / Export all). It does not include import; import will be proposed in a separate PR.

Context for maintainers: This work was first submitted in PR #2614, which received substantial review feedback focused on clean code, structure, and maintainability. We took that feedback seriously and ran a deliberate refactor pass (Uncle Bob / Clean Code) over the DrawIO-related code. This PR re-submits the same feature set with a cleaner, refactored codebase that we believe is easier to review and maintain. We’re grateful for the earlier review and have aimed to address those concerns in this iteration.


What’s in this PR

1. Generators (@likec4/generators)

  • generate-drawio.ts — Export single or multiple views to Draw.io XML; layout, styles, round-trip comments. Refactors: computeDiagramLayout split into smaller helpers; getViewDescriptionString extracted; buildNodeCellXml / buildEdgeCellXml and constants (SOLID/DRY/KISS); exported type DrawioViewModelLike.
  • parse-drawio.ts — Round-trip comment parsing and parse-to-LikeC4 for future import. Refactors: parseDrawioToLikeC4Multi split into mergeDiagramStatesIntoMaps, buildRootsFromFqnToCell, emitMultiDiagramModel (orchestrator ~50–60 lines); buildViewBlockLines / escapeLikec4Quotes; O(n²) deduplication replaced with parsedIds Set; UserObject fullTag uses innerXml so sibling <data> is preserved.
  • Tests: generate-drawio.spec.ts, parse-drawio.spec.ts; snapshots in __snapshots__/. Decompress error assertion accepts (base64 decode|inflate|URI decode) for Node/env behavior; snapshots updated for CI.

2. CLI (@likec4/likec4)

  • export/drawio/handler.tslikec4 export drawio with --outdir/-o, --all-in-one, --roundtrip, --uncompressed, --project, --use-dot. Uses DEFAULT_DRAWIO_ALL_FILENAME from @likec4/generators (DRY). Phase comments and thin handler pattern aligned with other export commands.
  • export/png/handler.ts — Type PngExportArgs, runExportPng(args, logger); PNG export supports --outdir/-o (docs updated).
  • No likec4 import drawio in this PR (no packages/likec4/src/cli/import/).

3. Playground

  • DrawIO context menu export only: Export view…, Export all… (DrawioContextMenuProvider, DrawioContextMenuDropdown, useDrawioContextMenuActions). Uses generateDrawio / generateDrawioMulti and parseDrawioRoundtripComments. No Import menu item or file input.
  • Monaco: only “Export to DrawIO” in editor context menu.

4. Documentation

  • drawio.mdx — Export only: mapping, options, multi-diagram, troubleshooting, re-export via comment blocks.
  • cli.mdx / docker.mdx — Export to DrawIO and PNG --outdir; no Import section.

5. E2E & tests

  • e2e/tests/drawio-playground.spec.ts — DrawIO menu (Export view / Export all). Run with playwright.playground.config.ts.
  • likec4: drawio-demo-export-import.spec.ts, drawio-tutorial-export-import.spec.ts — export tests; import/round-trip tests skipped in this PR.

What’s not in this PR

  • No likec4 import drawio command.
  • No Playground “Import from DrawIO” or Monaco Import action.
  • No docs for importing from Draw.io.
  • Import/round-trip tests remain skipped until the import PR.

Refactor summary (response to PR likec4#2614 feedback)

After the initial submission (PR likec4#2614), we applied a structured clean-code pass:

  • Generators: Smaller, single-responsibility functions; named types (DrawioViewModelLike); constants; phase comments; DRY (e.g. DEFAULT_DRAWIO_ALL_FILENAME, shared helpers).
  • CLI: Thin handlers; args types and runExport*(args, logger) pattern for drawio/PNG/JSON; consistent error handling and JSDoc.
  • Playground: Clear separation in useDrawioContextMenuActions (fillFromLayoutedModel, fillFromViewStates, etc.); constants for fonts/sizes.
  • E2E: Shared helpers (selectors, timeouts) in e2e/helpers/.

We believe this version is in better shape for review and long-term maintenance.


Correções após review (PR #11)

Incorporadas as sugestões do review anterior:

  • Changesets: Escopo export-only; removidas menções a import nos textos de release (drawio-implementation-plan, drawio-import-postpack).
  • DrawioContextMenuProvider: Valor do context api memoizado com useMemo(..., [actions.openMenu]) para evitar re-renders desnecessários.
  • LanguageClientSync: Uso de LayoutView.req em sendRequest (type-safe; removido cast manual).
  • E2E likec4-cli-export-drawio: Teste "empty workspace" agora verifica exitCode === 1 no erro rejeitado.
  • packages/config filenames.ts: basename remove barras finais (/ e \) e trata resultado vazio com fallback.
  • generate-drawio.ts: arcSize=12 (inteiro percentagem) em vez de 0.12 para cantos ligeiramente arredondados no Draw.io.
  • parse-drawio.ts: Lógica comum extraída para buildCommonDiagramStateFromCells; buildSingleDiagramState e buildDiagramState reutilizam (DRY).
  • drawio handler: useDot lido dos args e passado a runExportDrawio; graphviz binary vs wasm conforme flag --use-dot.
  • json handler: Campo useDot nos args e em runExportJson; guard quando projectsModels.length === 0 (warn + throw).

Alterações adicionais (nitpicks e robustez)

  • Changeset drawio-import-postpack: Texto reescrito para descrever apenas export e postpack; removidas menções a import/parser/CLI no bullet do Draw.io.
  • LanguageClientSync: requestLayoutView passa a usar LayoutView.req em sendRequest (type-safe; params e resposta inferidos; cast removido).
  • PNG handler: Bloco que inicia o servidor (viteDev) e o ciclo de export passou para dentro do mesmo try; finally garante server.close() mesmo quando resolveServerUrl ou o export lançam (evita leak do ViteDevServer).
  • Drawio handler: readWorkspaceSourceContent com proteção a ciclos de symlink: realpath + visitedDirs para não seguir o mesmo diretório duas vezes.
  • Comentários: DrawioContextMenuProvider (useCallbackRef evita churn do listener); generate-drawio.spec (navigateTo/ProcessedView); parse-drawio (collectRoundtrip quatro passagens); parse-drawio.spec (fallback getFirstDiagram).

Checklist

  • I have read the latest contribution guidelines.
  • My branch is synced with main (merge/rebase as appropriate).
  • Commit messages follow Conventional Commits (e.g. feat:, refactor:, test:).
  • Tests added/updated; import-related tests skipped in this branch. pnpm ci:test (Vitest) passes.
  • Documentation updated (drawio.mdx, cli.mdx, docker.mdx for export only).
  • Changesets can be added for user-facing packages if maintainers request.

Verification

  • pnpm build (filter !./apps/*), pnpm typecheck, pnpm test (or pnpm ci:test) — pass.
  • E2E drawio-playground: run with playground Playwright config when validating.

Notes for reviewers

  • Export behavior is unchanged from the original feature; changes are structural (refactors, types, constants, tests).
  • Generators: DrawioViewModelLike is the public type for view models passed to generateDrawio / generateDrawioMulti.
  • CLI: Drawio and PNG export handlers follow the same pattern as other export commands (args type + runExport* + thin handler).
  • We’re happy to address any further feedback and to add a changeset for the export feature if desired.

Reference

  • Initial submission and review context: PR #2614.
  • Original DrawIO bidirectional discussion: PR #2593. This PR is export-only; import will follow in a separate PR.

Summary by CodeRabbit

  • New Features

    • Draw.io export now preserves detailed layout metadata (layout, stroke, waypoints) for reliable round-trip exports.
  • Bug Fixes

    • Improved export and server initialization validation to fail fast on invalid states.
    • Better directory traversal to avoid redundant visits and handle symlinks more robustly.
  • Performance

    • Stabilized export event listener to reduce unnecessary reattachments.
  • Documentation

    • Added clearer doc comments for PNG and JSON export workflows.

…ks (symlink guard, comments, arcSize)

Co-authored-by: Cursor <[email protected]>
@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 13, 2026

📝 Walkthrough

Walkthrough

This PR refines Draw.io export and related tooling: stabilizes playground export listeners, tightens layout request typing, improves Draw.io parsing/round-trip comment emission, deduplicates filesystem traversal, validates PNG export server startup, and updates test/docs comments.

Changes

Cohort / File(s) Summary
Changelog
\.changeset/drawio-import-postpack.md
Expanded Draw.io export description to mention .drawio export, round-trip comment blocks (layout, stroke, waypoints), and postpack behavior.
Playground UI
apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx
Stabilized DRAWIO_EXPORT_EVENT listener by using a stable callback ref to avoid reattaching on handler identity changes.
Language Client / Typing
apps/playground/src/monaco/LanguageClientSync.tsx
Strengthened requestLayoutView signature to use explicit LayoutView request/params/response types instead of a generic method string.
Draw.io parsing & generation
packages/generators/src/drawio/parse-drawio.ts, packages/generators/src/drawio/generate-drawio.ts, packages/generators/src/drawio/*.spec.ts
Added helpers (isEdgeWithEndpoints, emitElementToLines, hex-to-name mapping), round-trip comment emitters for single/multi diagrams, buildDiagramState for multi-tab content, base64 helper, default-bbox check; updated test comments.
CLI export handlers
packages/likec4/src/cli/export/drawio/handler.ts, packages/likec4/src/cli/export/png/handler.ts
Deduplicate directory visits via realpath to avoid symlink cycles; add server URL validation after PNG server init and wrap startup in try.
Logging
packages/log/src/formatters.ts
Removed safeStringify; use loggable() for error message construction and adjust imports accordingly.
JSON export CLI docs
packages/likec4/src/cli/export/json/handler.ts
Added descriptive doc comments above JSON export function and subcommand registration.
CI config
.github/workflows/checks.yaml
Changed runner image from ubuntu-24.04 to ubuntu-24.04-arm across multiple jobs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nibbled code lines under moonlight glow,

Listeners steady, no more to-and-fro,
Types polite, they raise their paw and say,
"Layout requests — send true, not just a way."
Symlinks uncurled, servers checked with care — hop, hooray! 🥕

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

⚔️ .changeset/drawio-import-postpack.md (content)
⚔️ .github/workflows/checks.yaml (content)
⚔️ apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx (content)
⚔️ apps/playground/src/monaco/LanguageClientSync.tsx (content)
⚔️ packages/generators/src/drawio/generate-drawio.spec.ts (content)
⚔️ packages/generators/src/drawio/generate-drawio.ts (content)
⚔️ packages/generators/src/drawio/parse-drawio.spec.ts (content)
⚔️ packages/generators/src/drawio/parse-drawio.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)

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 and concisely summarizes the main change: adding export functionality for LikeC4 views to Draw.io format across multiple components (CLI, playground, docs, generators).
Description check ✅ Passed The description is comprehensive and well-structured, covering the feature scope, refactor rationale, detailed changes by package, what's excluded, and a complete verification checklist with all items marked as completed.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% 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
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/drawio-export
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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

…-24.04-arm; chore: JSDoc in drawio/json/png handlers and generators

Co-authored-by: Cursor <[email protected]>
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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/log/src/formatters.ts (1)

20-30: ⚠️ Potential issue | 🟡 Minor

Use switch (true) for this conditional chain.
The current if/else chain violates the style rule.

♻️ Suggested refactor
     .flatMap(([k, err]) => {
-      if (err instanceof Error) {
-        const mergedErr = mergeErrorCause(err)
-        if (mergedErr.stack) {
-          mergedErr.stack = parseStack(mergedErr.stack).join('\n')
-        }
-        return [mergedErr]
-      }
-      if (k === 'error' || k === 'err') {
-        return [new Error(loggable(err))]
-      }
-      return []
+      switch (true) {
+        case err instanceof Error: {
+          const mergedErr = mergeErrorCause(err)
+          if (mergedErr.stack) {
+            mergedErr.stack = parseStack(mergedErr.stack).join('\n')
+          }
+          return [mergedErr]
+        }
+        case k === 'error' || k === 'err':
+          return [new Error(loggable(err))]
+        default:
+          return []
+      }
     })

As per coding guidelines, Favor switch(true) over if-else chains.

🤖 Fix all issues with AI agents
In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 96-99: The type guard isEdgeWithEndpoints currently allows empty
strings for source/target, which contradicts the "non-empty" intent and can leak
into FQN lookups; update the guard to ensure both c.source and c.target are
non-empty strings (e.g., check typeof === 'string' && c.source.trim().length > 0
and same for c.target) and keep the doc comment in sync to reflect that
endpoints must be non-empty, referring to the isEdgeWithEndpoints function and
the DrawioCell/DrawioEdgeWithEndpoints types.
🧹 Nitpick comments (1)
.github/workflows/checks.yaml (1)

16-16: Switching to ARM runners—verify e2e compatibility.

This change moves all Linux jobs from x64 to ARM64 runners (ubuntu-24.04-arm). While ARM runners are typically cost-effective and Node.js/pnpm work fine on ARM, please verify:

  1. Playwright e2e tests (line 148): Confirm that Chromium installation and Playwright tests pass on ARM. ARM Linux support exists but has historically been less battle-tested.
  2. Native dependencies: Ensure any native Node modules used in the build compile correctly on ARM64.

Also, this infrastructure change appears unrelated to the PR's stated objective (Draw.io export). Consider documenting this in the PR description or splitting into a separate PR for clearer change tracking.

,

Also applies to: 38-38, 86-86, 104-104, 126-126, 148-148, 215-215, 265-265

Comment on lines +96 to 99
/** Type guard: true when cell is an edge with non-empty source and target. */
function isEdgeWithEndpoints(c: DrawioCell): c is DrawioEdgeWithEndpoints {
return c.edge === true && typeof c.source === 'string' && typeof c.target === 'string'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type guard allows empty endpoints despite “non-empty” intent.

This accepts '' for source/target, which can leak through to FQN lookups. Tighten the guard or adjust the doc comment.

Suggested fix
 function isEdgeWithEndpoints(c: DrawioCell): c is DrawioEdgeWithEndpoints {
-  return c.edge === true && typeof c.source === 'string' && typeof c.target === 'string'
+  return c.edge === true &&
+    typeof c.source === 'string' && c.source.trim() !== '' &&
+    typeof c.target === 'string' && c.target.trim() !== ''
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Type guard: true when cell is an edge with non-empty source and target. */
function isEdgeWithEndpoints(c: DrawioCell): c is DrawioEdgeWithEndpoints {
return c.edge === true && typeof c.source === 'string' && typeof c.target === 'string'
}
/** Type guard: true when cell is an edge with non-empty source and target. */
function isEdgeWithEndpoints(c: DrawioCell): c is DrawioEdgeWithEndpoints {
return c.edge === true &&
typeof c.source === 'string' && c.source.trim() !== '' &&
typeof c.target === 'string' && c.target.trim() !== ''
}
🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/parse-drawio.ts` around lines 96 - 99, The
type guard isEdgeWithEndpoints currently allows empty strings for source/target,
which contradicts the "non-empty" intent and can leak into FQN lookups; update
the guard to ensure both c.source and c.target are non-empty strings (e.g.,
check typeof === 'string' && c.source.trim().length > 0 and same for c.target)
and keep the doc comment in sync to reflect that endpoints must be non-empty,
referring to the isEdgeWithEndpoints function and the
DrawioCell/DrawioEdgeWithEndpoints types.

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