feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io#17
feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io#17
Conversation
…, playground) Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughStabilizes Draw.io UI callbacks via a ref-based file cache, threads an optional ISO Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
No actionable comments were generated in the recent review. 🎉 Comment |
There was a problem hiding this comment.
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 | 🟡 MinorInconsistent 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:
succeededwill be0- The condition
succeeded === 0 && viewmodels.length > 0evaluates tofalse(since0 > 0is 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 acceptingmodifiedas a top-level parameter for clearer API.The current implementation extracts
modifiedfrom the first view's options, which works correctly since mxfile has a singlemodifiedattribute. However, this behavior may be surprising if a caller sets differentmodifiedvalues on different views.For clearer API semantics, consider accepting
modifiedas 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) }
…fillFromModelView dev warn Co-authored-by: Cursor <[email protected]>
…w modes Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
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 optionallayoutOverride,strokeColorByNodeId,strokeWidthByNodeId,edgeWaypoints, andcompressed.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.c4source can be applied. No import UI or CLI in this PR.packages/generators/src/drawio/index.ts— Public API:generateDrawio,generateDrawioMulti,GenerateDrawioOptions, plusgetAllDiagrams,parseDrawioRoundtripComments,parseDrawioToLikeC4,parseDrawioToLikeC4Multi(for roundtrip and for the future import PR).generate-drawio.spec.ts,parse-drawio.spec.ts; snapshots in__snapshots__/.2. CLI (
@likec4/likec4)packages/likec4/src/cli/export/drawio/handler.ts—likec4 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
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 withplaywright.playground.config.ts(playground on 5174); main e2e config ignores this test.What's not in this PR
likec4 import drawiocommand (nopackages/likec4/src/cli/import/).Post-review fixes (CodeRabbit)
Addresses CodeRabbit AI review (actionable + nitpicks):
Actionable
stripHtmlnow uses shareddecodeXmlEntities()(covers'and all five XML entities). UserObjectidin constructed mxCell tag is escaped withescapeXml(userObjId).collectRoundtripForStateconsolidated to a single pass overidToFqnwhere possible.--all-in-oneand zero views: warn and throwERR_NO_VIEWS_EXPORTED.ensureSingleProject()only called when no--projectis provided, so multi-project workspaces can use--project.languageServicescreated withawait usingso it is disposed on exit (file watchers/RPC cleaned up).startTakeScreenshotmoved inside the per-project loop so timing reported is per-project, not cumulative.Nitpicks / cleanup
extensionContextimport.const _exhaustive: never = node) so new node types require a handler.configureLogtapenow uses explicit generics instead ofany; typo fixgerErrorFromLogRecord→getErrorFromLogRecord.EMPTY_DRAWIO_SNAPSHOT; stablegetSourceContentvia ref to avoid churn on every snapshot.console.warnwhen a view is skipped infillFromModelView.modified?: stringinGenerateDrawioOptionsandwrapInMxFilefor deterministic output (tests/caching).Second batch (CodeRabbit follow-up)
getDecodedStyleanddecodeRootStyleFieldwrapdecodeURIComponentin try/catch so malformed percent-encoding (e.g.%ZZ) does not abort parsing; on error return raw string (or undefined/empty). TernarytitlePartclarified with parentheses:(desc || tech) ? ....sinks/loggersfrom spread (...restConfig) so overrides are not misleading.errorMessge→errorMessageinappendErrorToMessage; removed commented-out line.filesRef.current = filesmoved intouseEffect([files])for concurrent-safety.exportParamsexplicitly typed asExportDrawioParams; single zero-views guard for both all-in-one and per-view modes.createLikeC4Logger('c4:export').buildOptionsFromRoundtrip(viewId, roundtrip, overrides)extracted;buildDrawioExportOptionsForViewsparses source once and distributes options (parse-once optimization for many views).Checklist (contribution guidelines)
mainbefore creating this PR.feat:,docs:).pnpm testandpnpm typecheck(andpnpm test:e2efor e2e); all pass.Notes for reviewers
.drawiofile per view by default;--all-in-onefor all views as tabs;--roundtripapplies layout/waypoints from comment blocks;--uncompressedfor Draw.io desktop compatibility.Review context
The original DrawIO bidirectional work (branch
feat/drawio-bidirectional-playground) was reviewed upstream in likec4/likec4 PR #2593 — Fix 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.