Skip to content

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

Merged
sraphaz merged 131 commits intolikec4:mainfrom
sraphaz:main
Feb 14, 2026
Merged

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io#2622
sraphaz merged 131 commits intolikec4:mainfrom
sraphaz:main

Conversation

@sraphaz
Copy link
Copy Markdown
Collaborator

@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 #2614 feedback)

After the initial submission (PR #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.


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: per-view and all-in-one multi-diagram exports, round‑trip metadata support, compression options, and Playground export actions (including optional source-content round‑trip).
  • Documentation

    • New Draw.io tooling docs and CLI docs updated with --all-in-one, --roundtrip, --uncompressed, PNG option changes, and Playground troubleshooting.
  • Tests

    • New Playwright and Vitest E2E suites for Playground, docs smoke, static navigation, and CLI Draw.io export.
  • Improvements

    • Cross‑platform postpack flows, improved logging/error handling, and more robust export compatibility.

sraphaz and others added 30 commits February 11, 2026 11:50
- Docs: Export DrawIO option --output,-o -> --outdir,-o
- generate-drawio: pageScale="1" for well-formed XML, mxUserObject after mxGeometry
- generate-drawio: flattenMarkdownOrString and isEmptyish for description/technology
- parse-drawio: infer system only for swimlane not shape=rectangle+rounded
- parse-drawio: toId remove dot from allowed chars to avoid invalid FQN
- Playground: DrawioContextMenu refactor with hook and presentational component

Co-authored-by: Cursor <[email protected]>
…d export/import

- Export: view title/description (diagram name + likec4ViewTitle/Description), summary, links, border, opacity, relationship kind/notation, likec4ColorName
- Import: diagram name as view id, view title/description from root cell, edge strokeColor to style color, opacity, shape (cylinder/document), summary/links/border/relationshipKind/notation
- Add CONVERSION-MAPPING.md documenting mapped and unmapped items
- Add pako.d.ts for typecheck; export DiagramInfo from parse-drawio

Co-authored-by: Cursor <[email protected]>
…pport

- IMPLEMENTATION-PLAN.md: phased plan for full conversion coverage
- parseDrawioToLikeC4Multi(xml): merge multiple diagrams into one model, one view per tab with include list
- getAllDiagrams(xml): extract all diagram name/id/content from mxfile
- First diagram without name attribute still yields view 'index' for backward compatibility
- Test: two-tab drawio produces view overview (include A, B) and view detail (include A, C)

Co-authored-by: Cursor <[email protected]>
…and single English doc

- Import: emit layout as comment block (likec4.layout.drawio) for round-trip
- Import: emit vertex strokeColor as comment block (DSL has no element strokeColor)
- Import: parse likec4Metadata on edges, emit metadata { } block
- Import: parse Draw.io native style 'link', emit element link when no likec4Links
- Export: add likec4StrokeColor on vertex, likec4Metadata on edge
- Docs: single Draw.io integration page (tooling/drawio.mdx) in English; link from CLI
- Remove CONVERSION-MAPPING.md and IMPLEMENTATION-PLAN.md; add changeset

Co-authored-by: Cursor <[email protected]>
…on, size/padding/textSize/iconPosition

- 2.1: Import emit vertex strokeWidth as comment block (likec4.strokeWidth.vertices)
- 2.4: Export likec4ViewNotation on root cell when view has notation
- 2.5: Export/import element size, padding, textSize, iconPosition (likec4Size etc.)
- Export likec4StrokeWidth on vertex for round-trip
- Docs: drawio.mdx updated with new mappings

Co-authored-by: Cursor <[email protected]>
- Export: GenerateDrawioOptions with layoutOverride, strokeColorByNodeId,
  strokeWidthByNodeId, edgeWaypoints; view notation string; customData and
  waypoints in XML.
- Import: view notation and edge waypoints comment blocks (single + multi);
  parseDrawioRoundtripComments() to read comment blocks for re-export.
- Playground: getSourceContent from workspace files; export applies round-trip
  options when comment blocks present.
- Playground generate: use PowerShell on Windows (usePowerShell + quotePowerShell)
  so generate runs without bash.
- Generators: export getAllDiagrams, DrawioRoundtripData, parseDrawioRoundtripComments;
  @types/pako; fix view.notation type and layoutByView possibly undefined.
- Docs: drawio.mdx updated with options, waypoints, re-export note.
- Tests: parseDrawioRoundtripComments; generate snapshots updated.
- Changeset: drawio-roundtrip-options.md.

Co-authored-by: Cursor <[email protected]>
- CLI: likec4 export drawio --roundtrip reads .c4 source, parses comment
  blocks (layout, stroke, waypoints), applies options per view.
- Generators: export generateDrawioMulti, parseDrawioRoundtripComments,
  parseDrawioToLikeC4Multi from main index for CLI/consumers.
- Playground: DrawioContextMenu accepts optional getSourceContent for
  round-trip export when used with explicit props.
- Docs: cli.mdx documents --roundtrip and --all-in-one for Export to DrawIO.
- E2E: playwright.playground.config.ts and drawio-playground.spec.ts;
  test:playground script; e2e/.gitignore allows drawio-playground.spec.ts.
- Changeset: drawio-cli-roundtrip-e2e.md (likec4, e2e patch).

Co-authored-by: Cursor <[email protected]>
- Use BBox, ThemeColorValues, RelationshipColorValues from @likec4/core
- Add getEffectiveStyles(viewmodel) and resolveThemeColor for DRY
- JSDoc for public and key internal helpers
- Docs: drawio.mdx export behavior (theme colors, containers behind, edge anchors, layoutOverride BBox)

Co-authored-by: Cursor <[email protected]>
…ut fallbacks

- useDrawioContextMenuActions: build viewModels from likec4model.views() then fill via getLayoutedModel, viewStates, layoutViews so all tabs export
- DrawioContextMenuDropdown: minor copy/tooltip tweaks
- README and schema updates

Co-authored-by: Cursor <[email protected]>
Export:
- Container: keep rounded=0 (no rounded corners)
- Elements: rounded=1 + arcSize=0.12 (subtle curve)
- UserObject with link for navigateTo; style link=data:page/id,likec4-<viewId>
- Docs: navigability and link format

Import:
- Parse UserObject wrapping mxCell: use id and link from UserObject
- navigateTo from link attribute (data:page/id,likec4-<viewId>) for round-trip
- Refactor: buildCellFromMxCell for reuse; skip duplicate id from UserObject

Playground:
- DrawIO context menu font size 10px

Co-authored-by: Cursor <[email protected]>
- Parse: container=1 -> system, child of container -> component; fillOpacity/likec4Opacity; view title/description/notation from root cell
- Merge container title cells (text shape) into container element; exclude from vertex list
- inferKind(style, parentCell); single and multi-diagram use containerIdToTitle and byId
- generate-drawio.spec: ProcessedView type for viewWithNav; drawio-demo: expected nodes include container title count
- CLI: likec4 import drawio described as experimental (import not yet fully validated)

Co-authored-by: Cursor <[email protected]>
- Add devops/commands/postpack.ts: copy packed tgz to package.tgz (Node fs)
- Replace cp ... || true in all 14 packages and styled-system with likec4ops postpack
- Enables pnpm lint:package and validate on Windows

Co-authored-by: Cursor <[email protected]>
- CI: single check-validate job runs pnpm validate (same as local); e2e/docs/playground depend on it
- Root: pnpm validate script (generate -> typecheck -> core type tests -> lint -> build -> lint:package -> test)
- Husky: pre-push runs validate when pushing to main
- AGENTS.md: document pnpm validate
- Changeset: drawio import alignment, CLI experimental, postpack, validate pipeline

Co-authored-by: Cursor <[email protected]>
- Export: preserve node parent links (parentId from view hierarchy); container title cell parent = container id
- Docs: mention --all-in-one early in Export section
- Playground: use layouted diagram for single-view export when available; forEach -> for...of
- generate-drawio: hex regex 3/4/6/8 only; remove sourcePoint/targetPoint from Array points; container margins: horizontal = xl, vertical = xl+md (vertical slightly larger)
- Remove pako.d.ts, use @types/pako
- parse-drawio: likec4LineType only 1 1 / 2 2 = dotted; getAllDiagrams guard empty/invalid content; DRY: stripHtml, stripHtmlForTitle, emitElementToLines, emitEdgesToLines, emitRoundtripComments(Single|Multi)
- worker: errToString guard JSON.stringify circular refs
- drawio-demo spec: assert reimportViews.length > 0; fix NodeId type in containerCount
- Snapshots updated for parent/container changes

Co-authored-by: Cursor <[email protected]>
- generate-drawio: use view.bounds for page, zero offset when content fits
- drawio-demo spec: increase timeout to 15s for vice versa test
- handler: use loggable(err) instead of String(err) for error logging
- worker: avoid String(err) on object in errToString (return literal)

Co-authored-by: Cursor <[email protected]>
…iew LSP request, MonacoEditor props

Co-authored-by: Cursor <[email protected]>
- Wait for .react-flow.initialized (20s) like export snapshot tests
- Increase test timeout to 35s for CI layout delay
- Remove arbitrary sleep and fragile canvas wait

Co-authored-by: Cursor <[email protected]>
- Remove CLI import drawio command and handlers
- Playground: DrawIO menu export only (no Import item, no Monaco import action)
- Docs: export-only sections in drawio.mdx and cli.mdx
- Tests: skip import/round-trip specs (to re-enable in import PR)
- E2E: assert Export to DrawIO and Export all only
- Add DRAWIO-PR-SPLIT.md and .pr-description-export.md / .pr-description-import.md

Co-authored-by: Cursor <[email protected]>
@sraphaz sraphaz marked this pull request as ready for review February 14, 2026 13:22
@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.

Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 160-163: getDecodedStyle and decodeRootStyleField call
decodeURIComponent which can throw on malformed percent-encodings and will abort
parsing (affects buildCellOptionalFields calling getDecodedStyle ~20x per cell);
wrap the decodeURIComponent usage in a try/catch and on error return the raw
un-decoded string (or undefined if original was empty) instead of allowing the
exception to propagate, and do the same in decodeRootStyleField so malformed
style values are ignored/returned safely and parsing continues; reference
getDecodedStyle, decodeRootStyleField and buildCellOptionalFields when making
this change.
🧹 Nitpick comments (10)
packages/log/src/index.ts (1)

70-73: Nit: ...config spreads sinks and loggers that are immediately overwritten.

On line 72, ...config may include sinks and loggers from the caller, but both are unconditionally overridden on lines 73–74. This is harmless but slightly misleading to readers. Consider destructuring to exclude them:

Proposed refactor
   const sinks = config?.sinks ?? {}
+  const { sinks: _sinks, loggers: _loggers, ...restConfig } = config ?? {}
   const sinksWithConsole: Record<TSinkId | 'console', Sink> = {
     console: getConsoleSink(),
     ...sinks,
   } as Record<TSinkId | 'console', Sink>
   configureLogtape<TSinkId | 'console', TFilterId>({
     reset: true,
-    ...config,
+    ...restConfig,
     sinks: sinksWithConsole,
apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx (2)

88-93: Ref assignment during render — consider moving to useEffect for concurrent-safety.

Writing filesRef.current = files during render can be problematic if React discards a render (Suspense, transitions in concurrent mode). Since the ref is only read from event handlers (not render), the practical risk is low, but moving the assignment into a useEffect is the React-recommended pattern.

Proposed fix
  const filesRef = useRef(files)
-  filesRef.current = files
+  useEffect(() => {
+    filesRef.current = files
+  }, [files])
   const getSourceContent = useCallback(() => {
     const contents = Object.values(filesRef.current).filter(Boolean)
     return contents.length > 0 ? contents.join('\n\n') : undefined
   }, [])

95-105: Conditional spread is fine but slightly obscure — consider explicit assignment for readability.

The ...(condition && { ... }) pattern works but can be non-obvious at a glance. This is purely a readability nit; no functional issue.

packages/likec4/src/cli/export/drawio/handler.ts (1)

258-265: Add an explicit type annotation to exportParams for compile-time safety.

If a field is later added to ExportDrawioParams, this untyped object literal won't produce a compile error. Adding satisfies ExportDrawioParams (or a : ExportDrawioParams annotation) catches drift early.

Suggested change
-  const exportParams = {
+  const exportParams: ExportDrawioParams = {
     viewmodels,
     outdir: args.outdir,
     workspacePath: args.path,
     roundtrip: args.roundtrip,
     uncompressed: args.uncompressed,
     logger,
   }
packages/log/src/formatters.ts (1)

107-107: Remove commented-out code.

Line 107 (// const format = options?.format) is a leftover artifact that adds noise.

Proposed fix
-  // const format = options?.format
packages/likec4/src/cli/export/png/handler.ts (1)

236-239: Minor: pngHandler creates a logger with prefix 'export' while JSON uses 'c4:export'.

The logger prefix inconsistency ('export' here vs 'c4:export' in the JSON handler at line 122 of json/handler.ts) produces different log output formatting. Consider aligning them if a consistent prefix is desired.

packages/generators/src/drawio/parse-drawio.ts (3)

983-989: Empty elements still emit { } block — is this intentional?

When hasBody is false, the code emits an element header followed by an empty { } block. In LikeC4, a simple declaration without braces (myElem = container 'Title') is valid and more concise. If the empty block is required by the parser, this is fine — otherwise it adds visual noise to generated output.


1019-1025: Operator precedence in titlePart ternary chain is hard to read.

const titlePart = title ? ` '${title}'` : desc || tech ? ` ''` : ''

The || binds tighter than ?:, so this works correctly as title ? ... : (desc || tech) ? ... : ''. However, adding parentheses would improve clarity.

♻️ Suggested clarification
-    const titlePart = title ? ` '${title}'` : desc || tech ? ` ''` : ''
+    const titlePart = title ? ` '${title}'` : (desc || tech) ? ` ''` : ''

1438-1456: DiagramState is declared after its first usage.

CommonDiagramStateFromCells at Line 1314 references DiagramState, which is defined 120+ lines later at Line 1438. TypeScript hoists type declarations so this compiles, but it hurts readability. Consider moving the DiagramState and SingleDiagramState type declarations before their first use (before buildCommonDiagramStateFromCells).

packages/generators/src/drawio/generate-drawio.ts (1)

1293-1314: buildDrawioExportOptionsFromSource parses round-trip once and distributes — clean design.

One minor observation: buildDrawioExportOptionsForViews (Line 1324) calls buildDrawioExportOptionsFromSource per view, which re-parses the entire .c4 source each time. For projects with many views, parsing once and distributing would be more efficient.

♻️ Suggested optimization
 export function buildDrawioExportOptionsForViews(
   viewIds: string[],
   sourceContent: string | undefined,
   overrides?: Partial<GenerateDrawioOptions>,
 ): Record<string, GenerateDrawioOptions> {
+  if (!sourceContent) {
+    return Object.fromEntries(viewIds.map(id => [id, { compressed: false, ...overrides }]))
+  }
+  const roundtrip = parseDrawioRoundtripComments(sourceContent)
   return Object.fromEntries(
     viewIds.map(viewId => [
       viewId,
-      buildDrawioExportOptionsFromSource(viewId, sourceContent, overrides),
+      buildOptionsFromRoundtrip(viewId, roundtrip, overrides),
     ]),
   )
 }

Extract a helper that takes the already-parsed DrawioRoundtripData | null so parseDrawioRoundtripComments runs once.

…filesRef useEffect, exportParams type, formatters typo, titlePart parens, png logger, buildOptionsFromRoundtrip parse-once)

Co-authored-by: Cursor <[email protected]>
@sraphaz sraphaz marked this pull request as draft February 14, 2026 13:41
feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io
@sraphaz sraphaz marked this pull request as ready for review February 14, 2026 13:44
@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.

Copy link
Copy Markdown
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 107-123: The getAttr function can match attribute name suffixes
(e.g., matching "userid=" when searching for "id="); fix getAttr by ensuring the
found needle has a word-boundary before it: after finding start via indexOf,
verify that start === 0 or the character at start - 1 is whitespace (space, tab,
newline) before accepting the match, and only then parse the quoted value as
currently done; update the loop in getAttr to continue searching if the
preceding character is not a valid boundary so it returns the correct attribute
value.

In `@PR-DESCRIPTION.md`:
- Around line 47-49: Edit the three consecutive bullets in PR-DESCRIPTION.md so
they don't all start with "No": keep the first as-is and reword the next two for
variety and clarity (for example, change the second to "Missing `likec4 import
drawio` command (no `packages/likec4/src/cli/import/`)" and the third to
"Playground lacks an 'Import from DrawIO' menu item, file input, or Monaco
Import action", or combine related points into a single sentence) — update the
three bullet lines shown in the diff accordingly to improve readability.
🧹 Nitpick comments (7)
apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx (1)

75-75: usePlayground() is called but its return value is never used.

playground is assigned on Line 75 but never referenced in the component body. This is dead code that also triggers hook execution unnecessarily.

Proposed fix
-  const playground = usePlayground()

And remove usePlayground from the import on Line 1:

-import { usePlayground, usePlaygroundSnapshot } from '$/hooks/usePlayground'
+import { usePlaygroundSnapshot } from '$/hooks/usePlayground'
#!/bin/bash
# Verify that `playground` is not referenced elsewhere in this file
rg -n '\bplayground\b' -- "apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx"
packages/likec4/src/cli/export/drawio/handler.ts (1)

86-107: Symlinked source directories are silently skipped during round-trip reads.

e.isDirectory() returns false for symlinks pointing to directories, so walk never descends into them. If a workspace uses symlinked source folders (e.g., monorepo setups), their .c4/.likec4 files won't be included in the round-trip content.

If this is intentional for safety, a brief comment explaining the choice would help. Otherwise, consider adding an e.isSymbolicLink() check that resolves the target and recurses if it's a directory (the visitedDirs/realpath cycle guard already handles loops).

packages/likec4/src/cli/export/png/handler.ts (2)

334-356: Minor: inconsistent access style for max-attempts / maxAttempts.

Line 336 accesses the yargs arg as args['max-attempts'], while line 346 uses the camelCase form args.maxAttempts. Both work due to yargs' automatic camelCase aliasing, but mixing styles in the same handler is a readability nit.

Suggested consistency fix
-      invariant(args['max-attempts'] >= 1, 'max-attempts must be >= 1')
+      invariant(args.maxAttempts >= 1, 'max-attempts must be >= 1')

18-40: Consider documenting all public fields on PngExportArgs.

Several fields (project, theme, useDotBin, timeoutMs, maxAttempts, filter) lack JSDoc while others have it. For a publicly exported type this is a minor inconsistency. As per coding guidelines, "Use JSDoc to document public classes and methods".

packages/generators/src/drawio/parse-drawio.ts (2)

1447-1466: DiagramState type is defined after its first usage via CommonDiagramStateFromCells.

CommonDiagramStateFromCells at line 1324 is DiagramState & { byId: ... }, but DiagramState is declared at line 1448. TypeScript hoists type declarations so this compiles fine, but it hurts readability — the reader encounters the alias before the definition.


646-689: computeContainerTitles uses .find() — only the first matching text shape near a container is considered.

If multiple text-shape cells overlap the title area of a container, only the first one found (in array order) becomes the title cell. Remaining text shapes near the container's top would not be filtered as title cells and could become spurious elements. This is fine for LikeC4-generated drawio files but may produce unexpected results for hand-crafted diagrams with multiple overlapping labels.

packages/generators/src/drawio/generate-drawio.ts (1)

86-96: Accessing optional DSL fields via as casts — pragmatic but fragile.

The ExtendedNode type and nodeOptionalFields / edgeOptionalFields accessors cast Node to a wider type to access fields (notes, summary, tags, navigateTo, etc.) that may or may not exist on the runtime object. This works today but is invisible to the type system — if upstream types change, these casts won't produce compile errors.

Consider adding a runtime guard (e.g., 'notes' in node) or using a shared type from @likec4/core if one becomes available.

Also applies to: 104-133

Comment on lines 107 to 123
function getAttr(attrs: string, name: string): string | undefined {
const re = new RegExp(`${name}="([^"]*)"`, 'i')
const m = attrs.match(re)
return m ? m[1] : undefined
const needle = `${name}=`
const lower = attrs.toLowerCase()
const needleLower = needle.toLowerCase()
let i = 0
while (i < attrs.length) {
const start = lower.indexOf(needleLower, i)
if (start === -1) return undefined
const quoteStart = start + needle.length
if (quoteStart < attrs.length && attrs[quoteStart] === '"') {
const valueEnd = attrs.indexOf('"', quoteStart + 1)
if (valueEnd !== -1) return attrs.slice(quoteStart + 1, valueEnd)
}
i = start + 1
}
return undefined
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getAttr can match attribute names that are suffixes of longer attributes.

The indexOf-based search for name= doesn't enforce a word boundary before the match. For example, searching for "id" in userid="foo" id="bar" would match userid= first and return "foo" instead of "bar".

In practice, draw.io's standard attribute names (id, vertex, edge, style, etc.) don't collide with each other as suffixes, so this is safe for current usage. However, if the file ever needs to parse custom attributes or third-party draw.io extensions, this could silently produce wrong values.

🛡️ Proposed fix — check for word boundary (space or start-of-string) before the needle
   let i = 0
   while (i < attrs.length) {
     const start = lower.indexOf(needleLower, i)
     if (start === -1) return undefined
+    // Ensure we matched a full attribute name, not a suffix of a longer name
+    if (start > 0 && attrs[start - 1] !== ' ' && attrs[start - 1] !== '\t' && attrs[start - 1] !== '\n') {
+      i = start + 1
+      continue
+    }
     const quoteStart = start + needle.length
     if (quoteStart < attrs.length && attrs[quoteStart] === '"') {
🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/parse-drawio.ts` around lines 107 - 123, The
getAttr function can match attribute name suffixes (e.g., matching "userid="
when searching for "id="); fix getAttr by ensuring the found needle has a
word-boundary before it: after finding start via indexOf, verify that start ===
0 or the character at start - 1 is whitespace (space, tab, newline) before
accepting the match, and only then parse the quoted value as currently done;
update the loop in getAttr to continue searching if the preceding character is
not a valid boundary so it returns the correct attribute value.

sraphaz and others added 4 commits February 14, 2026 10:59
…d (DrawioContextMenuProvider); remove ad-hoc PR docs

Co-authored-by: Cursor <[email protected]>
feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io
@sraphaz sraphaz marked this pull request as draft February 14, 2026 14:14
@sraphaz sraphaz marked this pull request as ready for review February 14, 2026 14:14
@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.

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.

2 participants