Skip to content

feat(playground,cli,docs): DrawIO bidirecional e menu no Playground#2592

Merged
davydkov merged 2 commits intolikec4:mainfrom
sraphaz:feat/drawio-bidirectional-playground
Feb 11, 2026
Merged

feat(playground,cli,docs): DrawIO bidirecional e menu no Playground#2592
davydkov merged 2 commits intolikec4:mainfrom
sraphaz:feat/drawio-bidirectional-playground

Conversation

@sraphaz
Copy link
Copy Markdown
Collaborator

@sraphaz sraphaz commented Feb 11, 2026

DrawIO: conversão bidirecional e integração no Playground

Resumo

Implementação de conversão DrawIO ↔ LikeC4 (export/import) e integração no Playground: menu de contexto no diagrama e no editor Monaco com "Import from DrawIO" e "Export to DrawIO", documentação CLI atualizada e correções para dev (miniflare, source maps).


O que foi feito

1. Conversão DrawIO ↔ LikeC4 (já existente no repo, documentação reforçada)

  • Export (LikeC4 → DrawIO): likec4 export drawio [path] -o <outdir> — gera um .drawio por view; preserva posições, hierarquia, cores, descrição e tecnologia.
  • Import (DrawIO → LikeC4): likec4 import drawio <input.drawio> -o <outfile.c4> — converte diagrama DrawIO em código LikeC4 (elementos, relações, cores, propriedades customizadas).

2. Documentação CLI

  • apps/docs/src/content/docs/tooling/cli.mdx:
    • Introdução: incluído "Export to DrawIO" e "Import from DrawIO".
    • Nova seção Export to DrawIO (comando, opções, nota sobre Graphviz).
    • Nova seção Import from DrawIO (comando, opções, preservação de metadados).
    • Seção "Generate Mermaid, Dot, D2, PlantUml" com referência ao DrawIO.
  • packages/likec4/README.md: features e subseção "Export to DrawIO / Import from DrawIO" com exemplos.

3. Playground: menu DrawIO no diagrama e no editor

  • Menu de contexto no diagrama: clique direito no canvas abre menu com:
    • Import from DrawIO… — abre seletor de arquivo; converte .drawio em LikeC4 e adiciona como novo arquivo no workspace (ex.: imported.c4).
    • Export to DrawIO — exporta a view atual para .drawio e inicia download.
  • Menu de contexto no editor (Monaco): as mesmas ações foram adicionadas ao menu nativo do editor (junto com Go to Definition, Copy, Paste etc.) via editor.addAction() com contextMenuGroupId: '9_cutcopypaste'.
  • Provider compartilhado: DrawioContextMenuProvider envolve o layout do workspace; expõe openMenu(event) via contexto React; o diagrama usa onCanvasContextMenu e o painel do editor usa onContextMenu no wrapper. Constantes de evento em drawio-events.ts para o Monaco não importar o provider (evita dependência pesada no bundler).

4. Playground: suporte a novo arquivo (Import)

  • Evento workspace.addFile na máquina do playground: { filename, content } — atualiza files, originalFiles, activeFilename e persiste.
  • Monaco / LSP: quando activeFilename muda para um arquivo ainda não aberto (ex.: novo .c4 do import), LanguageClientSync chama ensureFileInWorkspace(), cria o modelo no editor, envia BuildDocuments ao LSP e requestComputedModel() para o diagrama atualizar.

5. Dependências e configuração Playground

  • @likec4/generators adicionado ao playground (para generateDrawio e parseDrawioToLikeC4).
  • miniflare adicionado como dependência explícita do playground para o @cloudflare/vite-plugin no dev (evita "Expected miniflare to be defined").
  • Vite (playground): plugin esbuild strip-vscode-textmate-sourcemap para remover //# sourceMappingURL do vscode-textmate na pré-bundlização e reduzir aviso de source map no console.

6. Arquivos principais alterados/criados

Área Arquivos
CLI / generators packages/generators (drawio), `packages/likec4/src/cli/export
Docs apps/docs/.../tooling/cli.mdx, packages/likec4/README.md
Playground UI apps/playground/src/components/drawio/ (DrawioContextMenuProvider, drawio-events, DrawioContextMenu), routes/w.$workspaceId/route.tsx, $viewId.tsx
Playground state apps/playground/src/state/playground-machine.ts (workspace.addFile)
Monaco apps/playground/src/monaco/LanguageClientSync.tsx (ações DrawIO + ensureFile + BuildDocuments), utils.ts (ensureFileInWorkspace), config (não alterado para DrawIO)
Config apps/playground/vite.config.ts (plugin sourcemap), apps/playground/package.json (generators, miniflare)

Checklist (template do repo)

  • Li as contribution guidelines.
  • Mensagens de commit seguem Conventional Commits.
  • Testes existentes para DrawIO em packages/generators (generate-drawio.spec.ts, parse-drawio.spec.ts); testes do playground são manuais (menu e import/export).
  • Documentação atualizada (CLI e README likec4).

Como testar

  1. Playground: pnpm install (raiz), pnpm dev em apps/playground. Abrir um workspace (ex.: tutorial).
  2. Import: Clique direito no diagrama ou no editor → "Import from DrawIO…" → escolher um .drawio → novo arquivo .c4 deve aparecer no editor e o diagrama atualizar.
  3. Export: Clique direito → "Export to DrawIO" → deve baixar <viewId>.drawio.
  4. CLI: likec4 export drawio -o ./out, likec4 import drawio file.drawio -o model.c4 (requer Graphviz para export com layout).

Checklist (template oficial do repo)

  • I've thoroughly read the latest contribution guidelines.
  • I've rebased my branch onto main before creating this PR.
  • My commit messages follow conventional spec
  • I've added tests to cover my changes (if applicable).
  • I've verified that all new and existing tests have passed locally for mobile, tablet, and desktop screen sizes.
  • My change requires documentation updates.
  • I've updated the documentation accordingly.

Summary by CodeRabbit

  • New Features

    • Added DrawIO export support: export diagrams as DrawIO files (.drawio format) via CLI.
    • Added DrawIO import support: convert DrawIO files (.drawio) into LikeC4 models via CLI.
    • Added context menu in playground: right-click canvas to quickly import/export DrawIO files.
  • Documentation

    • Expanded CLI documentation with DrawIO export/import examples and usage guidance.

sraphaz and others added 2 commits February 11, 2026 01:27
- Playground: menu de contexto no diagrama e no editor (Monaco) com
  Import from DrawIO e Export to DrawIO; DrawioContextMenuProvider e
  drawio-events.ts para evitar dependência pesada no Monaco.
- Playground: evento workspace.addFile; sync de novos arquivos com
  Monaco e LSP (ensureFileInWorkspace + BuildDocuments).
- Docs: seções Export to DrawIO e Import from DrawIO em tooling/cli.mdx
  e README do likec4.
- Playground: dependência miniflare para @cloudflare/vite-plugin;
  plugin Vite para remover sourceMappingURL do vscode-textmate.
- Playground: dependência @likec4/generators para generateDrawio e
  parseDrawioToLikeC4.

Co-authored-by: Cursor <[email protected]>
- generators: DrawioCell exactOptionalPropertyTypes, viewmodel.bounds fallback
- generators: DEFAULT_ELEMENT_COLORS['primary'], parse attrs default
- likec4 CLI: ProjectId for layoutedModel, error logging, input validation
- diagram: use execSync for panda ship (Windows without bash)
- icons: explicit entryPoints to avoid esbuild symlink read error
- language-server: cross-platform pregenerate (node -e instead of rm)

Co-authored-by: Cursor <[email protected]>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 11, 2026

⚠️ No Changeset found

Latest commit: 58f52a0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58f52a096f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return `<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="LikeC4" modified="${new Date().toISOString()}" agent="LikeC4" version="1.0" etag="" type="device">
<diagram name="${escapeXml(view.id)}" id="likec4-${escapeXml(view.id)}">
<mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale=1 pageWidth="827" pageHeight="1169" math="0" shadow="0">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Quote pageScale in generated mxGraphModel XML

This template emits pageScale=1 without quotes, so every exported DrawIO file is not well-formed XML at the <mxGraphModel> node. Standards-compliant XML parsers reject unquoted attribute values, which means downstream import/validation tooling can fail before reading any diagram content; this should be emitted as pageScale="1" like the other attributes.

Useful? React with 👍 / 👎.

watch: false,
})

likec4.ensureSingleProject()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor --project in drawio export flow

The command advertises a --project selector, but this unconditional ensureSingleProject() throws as soon as a workspace has multiple projects, before the selected project is resolved. In practice likec4 export drawio -p <project> still fails on multi-project workspaces, so the new option cannot be used in the scenario it is meant to support.

Useful? React with 👍 / 👎.

return name
.trim()
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9_.-]/g, '')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude dots when sanitizing imported element IDs

Keeping . in toId creates ambiguous identifiers for imported nodes: later code treats dots as hierarchy separators (fqn.split('.') in emitElement) and relation output still uses the full FQN, so labels like api.v1 can produce mismatched declarations/references and invalid LikeC4 source. This sanitizer should not preserve dots in a single element identifier.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

This PR adds DrawIO import/export capabilities across the LikeC4 tooling. New CLI commands enable diagram format conversion, playground UI gains DrawIO context menu integration, and generator functions provide bidirectional XML-to-model conversion. Documentation, dependencies, and build configurations are updated accordingly.

Changes

Cohort / File(s) Summary
DrawIO Generator Core
packages/generators/src/drawio/generate-drawio.ts, packages/generators/src/drawio/parse-drawio.ts, packages/generators/src/drawio/generate-drawio.spec.ts, packages/generators/src/drawio/parse-drawio.spec.ts
New XML-based serialization and deserialization logic; generateDrawio converts LikeC4 view models to DrawIO XML with color/shape mapping; parseDrawioToLikeC4 reverses the process. Comprehensive test coverage with snapshot assertions.
Generator Package Exports
packages/generators/src/drawio/index.ts, packages/generators/src/index.ts
Re-exports generateDrawio and parseDrawioToLikeC4 at package entry points for public consumption.
CLI Import/Export Commands
packages/likec4/src/cli/export/drawio/handler.ts, packages/likec4/src/cli/import/drawio/handler.ts, packages/likec4/src/cli/export/index.ts, packages/likec4/src/cli/import/index.ts, packages/likec4/src/cli/index.ts
New drawio export/import handlers wired into CLI; export iterates views and generates .drawio files; import converts .drawio input to .c4 LikeC4 models.
Playground DrawIO Context Menu
apps/playground/src/components/drawio/DrawioContextMenu.tsx, apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx, apps/playground/src/components/drawio/drawio-events.ts
New context menu UI with import/export actions; DrawioContextMenuProvider manages state and file I/O; drawio-events defines event constants for editor integration.
Playground Integration
apps/playground/src/monaco/LanguageClientSync.tsx, apps/playground/src/monaco/utils.ts, apps/playground/src/routes/w.$workspaceId/$viewId.tsx, apps/playground/src/routes/w.$workspaceId/route.tsx
Adds DrawIO editor actions, wires context menu to diagram canvas, introduces ensureFileInWorkspace utility for file provisioning.
Playground State & Dependencies
apps/playground/src/state/playground-machine.ts, apps/playground/package.json
New workspace.addFile event for adding imported files to workspace; adds miniflare and @likec4/generators dependencies.
Build & Tooling
apps/playground/vite.config.ts, packages/diagram/vite.config.ts, packages/icons/scripts/generate.mjs, packages/language-server/package.json
vite configs updated for sourcemap handling and shell execution; icon script adds dynamic index discovery; language-server pregenerate script made cross-platform.
Documentation
apps/docs/src/content/docs/tooling/cli.mdx, packages/likec4/README.md
CLI docs expanded with DrawIO export/import sections and usage examples; README updated to list DrawIO as export capability.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Canvas as Diagram<br/>Canvas
    participant Menu as DrawIO Context<br/>Menu
    participant Generator as DrawIO<br/>Generator
    participant FileSystem as File<br/>System

    User->>Canvas: Right-click
    Canvas->>Menu: onCanvasContextMenu
    Menu->>User: Show import/export options
    User->>Menu: Click "Export to DrawIO"
    Menu->>Generator: generateDrawio(viewmodel)
    Generator->>Generator: Map shapes, colors,<br/>layout to mxGraph
    Generator->>Menu: Return DrawIO XML
    Menu->>FileSystem: Download .drawio blob
    FileSystem->>User: Save file
Loading
sequenceDiagram
    actor User
    participant FileSystem as File<br/>System
    participant Menu as DrawIO Context<br/>Menu
    participant Parser as DrawIO<br/>Parser
    participant Workspace as Workspace<br/>State
    participant Editor as Code<br/>Editor

    User->>FileSystem: Select .drawio file
    FileSystem->>Menu: File input change
    Menu->>Parser: parseDrawioToLikeC4(xml)
    Parser->>Parser: Extract cells, edges,<br/>infer element kinds
    Parser->>Menu: Return LikeC4 source
    Menu->>Workspace: workspace.addFile(filename,<br/>content)
    Workspace->>Workspace: Update files,<br/>set active
    Workspace->>Editor: Display new .c4 file
    Editor->>User: Show imported model
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • davydkov

Poem

🐰 Hop, hop! DrawIO's here to stay,
Export and import come out to play,
From XML to LikeC4 we bound,
The finest diagram tools around!
With context menus and CLI commands bright,
The playground now shines with DrawIO delight! 🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title describes the main addition: DrawIO bidirectional conversion and menu integration in the Playground, covering the primary features implemented.
Description check ✅ Passed The PR description is comprehensive, covering all major changes (CLI, docs, Playground UI/state, Monaco integration, dependencies) with structured sections, testing instructions, and a completed checklist aligned with the repository template.

✏️ 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

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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: 10

🤖 Fix all issues with AI agents
In `@apps/docs/src/content/docs/tooling/cli.mdx`:
- Around line 198-204: The docs CLI table lists the DrawIO export flag as
`--output, -o` but the export DrawIO handler expects `outdir` (alias `-o`);
update the CLI docs entry to use `--outdir, -o` (keeping the same description
"Output directory for `.drawio` files (default: current directory or `path`)")
so the documentation matches the option defined in the export DrawIO handler
(option name `outdir`).

In `@apps/playground/src/components/drawio/DrawioContextMenu.tsx`:
- Around line 25-146: This duplicates import/export and menu UI between
DrawioContextMenu and DrawioContextMenuProvider; extract the shared logic into a
new hook (e.g., useDrawioActions) that encapsulates fileInputRef,
handleImportFile (FileReader → parseDrawioToLikeC4 → playground.actor.send
workspace.addFile) and handleExport (generateDrawio → Blob + download) and
expose import/export helpers and a trigger ref, then create a presentational
component (e.g., DrawioContextMenuView) that renders the Mantine Menu (uses
props for opened, onClose, menuPosition, onImport, onExport, disabled state) and
reuse it from both DrawioContextMenu and DrawioContextMenuProvider to eliminate
duplication.

In `@packages/generators/src/drawio/generate-drawio.ts`:
- Around line 159-161: The generated DrawIO XML places <mxUserObject> inside
<mxCell> via the userData string, which is non-standard; change the writer in
generate-drawio.ts so that when desc or tech exist you wrap the entire <mxCell>
element with an outer <mxUserObject> containing the two <data
key="likec4Description"> and <data key="likec4Technology"> tags instead of
injecting them as children; update the code that builds the cell string
(reference the userData variable) to emit the opening <mxUserObject> before the
<mxCell> and the closing </mxUserObject> after it, ensuring proper nesting and
that parse-drawio.ts (which scans <mxCell> inner content) still reads the
metadata or adjust parsing accordingly.
- Line 209: Locate the XML string in
packages/generators/src/drawio/generate-drawio.ts that contains the
'<mxGraphModel ... pageScale=1 ...>' element (the XML fragment used to generate
the draw.io document) and fix the malformed attribute by changing pageScale=1 to
pageScale="1" so all attributes are properly quoted; update any tests or string
builders that construct this element (e.g., the drawio template or the function
that emits the mxGraphModel fragment) to ensure the attribute gets emitted with
quotes.

In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 95-96: The fallbacks styleMap.get('fillColor') and
styleMap.get('strokeColor') are dead because keys are lowercased when populating
styleMap (k.toLowerCase()); update the code so the lookups are consistent:
either remove the dead fallbacks and only use styleMap.get('fillcolor') /
styleMap.get('strokecolor') when assigning the fillColor and strokeColor
variables, or stop lowercasing keys at the map population (remove
k.toLowerCase()) if you intended to preserve original casing; adjust the code
near the styleMap population and the fillColor/strokeColor assignments
accordingly.
- Around line 132-138: In inferKind, make the ambiguous boolean expression
explicit: if the intended rule is "swimlane OR (shape=rectangle AND rounded)"
change the condition to if (s.includes('swimlane') ||
(s.includes('shape=rectangle') && s.includes('rounded'))) return 'system'; if
instead "swimlane" should also require rounded change it to if
((s.includes('swimlane') && s.includes('rounded')) ||
(s.includes('shape=rectangle') && s.includes('rounded'))) return 'system'. Use
the inferKind function and the s.includes(...) checks to locate and update the
condition accordingly.

In `@packages/likec4/src/cli/export/drawio/handler.ts`:
- Line 38: The outdir assignment in handler.ts can become undefined because both
args.outdir and args.path are optional; update the code that sets outdir
(currently const outdir = args.outdir ?? args.path) to default to process.cwd()
when neither is provided (e.g., const outdir = args.outdir ?? args.path ??
process.cwd()), and ensure the subsequent mkdir and any other uses (the other
occurrence around the mkdir call at the later block) use this non-undefined
outdir so mkdir(outdir, ...) never receives undefined.
- Line 45: The graphviz config is using the option descriptor useDotBin (always
truthy) instead of the parsed CLI value, causing graphviz to be set to 'binary'
unconditionally; update the graphviz assignment in handler.ts to read the
resolved argument args['use-dot'] (or equivalent parsed boolean) rather than
useDotBin so that graphviz becomes 'binary' when args['use-dot'] is true and
'wasm' when false.

In `@packages/likec4/src/cli/import/drawio/handler.ts`:
- Line 3: The import list in handler.ts includes an unused symbol dirname;
remove dirname from the named import from 'node:path' so the statement imports
only used symbols (extname, relative, resolve) to eliminate the unused-import
warning and keep imports minimal.
- Around line 45-53: The handler currently computes outfile and calls
writeFile(outfile, likec4Source) without ensuring the parent directory exists,
which can cause ENOENT for nested paths; update the code after computing outfile
(variables: outfile, inputPath, extname) to compute the parent directory via
dirname(outfile) and call mkdir(parentDir, { recursive: true }) before writeFile
so the intermediate directories are created (use the existing dirname import and
the writeFile/mkdir functions).
🧹 Nitpick comments (11)
packages/language-server/package.json (1)

115-115: Consider simplifying with fs.rmSync.

The inline script works but is quite dense. A simpler cross-platform alternative that also handles nested contents:

♻️ Suggested simplification
-    "pregenerate": "node -e \"const fs=require('fs');const p=require('path');const d='src/generated';if(fs.existsSync(d)){fs.readdirSync(d).forEach(f=>{try{fs.unlinkSync(p.join(d,f))}catch(e){}})}\"",
+    "pregenerate": "node -e \"const fs=require('fs');const d='src/generated';if(fs.existsSync(d)){fs.rmSync(d,{recursive:true,force:true});fs.mkdirSync(d,{recursive:true})}\"",

This handles subdirectories too (current unlinkSync silently fails on directories), is shorter, and achieves the same cross-platform goal. fs.rmSync with recursive + force is available since Node 14.14.

packages/diagram/vite.config.ts (1)

126-129: execSync will throw on non-zero exit — consider wrapping with try/catch for consistency.

The err guard on line 121 gracefully skips on failure, but if pnpm panda ship itself exits non-zero, the unhandled exception will crash the build. If a graceful skip is intended here too, wrap in try/catch. Also, cwd: process.cwd() is the default for execSync and can be dropped.

♻️ Suggested fix
       this.info('shipping panda')
-      execSync('pnpm panda ship --outfile ./panda.buildinfo.json', {
-        stdio: 'inherit',
-        cwd: process.cwd(),
-      })
+      try {
+        execSync('pnpm panda ship --outfile ./panda.buildinfo.json', {
+          stdio: 'inherit',
+        })
+      } catch (e) {
+        this.warn('panda ship failed, skipped')
+      }
packages/icons/scripts/generate.mjs (1)

187-199: Consider reusing indexFiles at line 218 to avoid a duplicate crawl and ensure consistent filtering.

Lines 187–192 crawl **/index.ts with a node_modules filter, but line 218 performs the same crawl again without the filter. You could reuse indexFiles in the for loop at line 218 to avoid the redundant filesystem crawl and ensure node_modules paths are consistently excluded.

♻️ Proposed refactor
-for (const fname of new fdir().glob('**/index.ts').withBasePath().crawl().sync()) {
+for (const fname of indexFiles) {
apps/playground/src/state/playground-machine.ts (1)

394-409: Persist-before-assign means the newly added file isn't immediately saved.

The 'persist to storage' action runs before assign, so it writes the previous context (without the new file). If the user closes the tab before another persist trigger fires, the added file is lost. This matches the existing workspace.changeActiveFile pattern (Line 386), so it's consistent — but worth noting.

Consider swapping the order or adding a second persist after the assign if reliability of the import flow matters:

Suggested reorder
         'workspace.addFile': {
           actions: [
-            'persist to storage',
             assign({
               files: ({ event, context }) => ({
                 ...context.files,
                 [event.filename]: event.content,
               }),
               originalFiles: ({ event, context }) => ({
                 ...context.originalFiles,
                 [event.filename]: event.content,
               }),
               activeFilename: ({ event }) => event.filename,
             }),
+            'persist to storage',
           ],
         },
apps/playground/src/routes/w.$workspaceId/route.tsx (1)

2-3: Merge imports from the same module.

Both imports come from $components/drawio/DrawioContextMenuProvider and can be combined into a single import statement.

♻️ Proposed fix
-import { DrawioContextMenuProvider } from '$components/drawio/DrawioContextMenuProvider'
-import { useDrawioContextMenu } from '$components/drawio/DrawioContextMenuProvider'
+import { DrawioContextMenuProvider, useDrawioContextMenu } from '$components/drawio/DrawioContextMenuProvider'
packages/generators/src/drawio/parse-drawio.spec.ts (1)

49-55: Consider adding a test for truly invalid XML input.

The "empty XML" test covers a minimal but structurally unusual input (no <diagram>/<mxGraphModel> wrapper). It would be useful to also test behavior when given completely invalid XML (e.g. "not xml at all") to document whether parseDrawioToLikeC4 throws or returns a fallback.

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

48-52: mockViewModel wraps vi.fn but the mock is never asserted on.

Since you never check call counts or arguments, a plain function would be cleaner. Using vi.fn here implies the mock is tracked for assertions, which could confuse future readers.

♻️ Suggested simplification
-const mockViewModel = vi.fn(function ($view: ProcessedView<aux.Unknown>) {
-  return {
-    $view,
-  } as unknown as LikeC4ViewModel<aux.Unknown, LayoutedView<aux.Unknown>>
-})
+function mockViewModel($view: ProcessedView<aux.Unknown>) {
+  return {
+    $view,
+  } as unknown as LikeC4ViewModel<aux.Unknown, LayoutedView<aux.Unknown>>
+}
packages/generators/src/drawio/parse-drawio.ts (1)

28-32: Static analysis: regex built from variable input (ReDoS surface).

While name is always a hardcoded string from internal callers, the RegExp constructor with interpolated input is flagged by ast-grep as a potential ReDoS vector (CWE-1333). Since the callers are all internal and pass safe attribute names, the actual risk is negligible, but escaping the input would be a defensive improvement.

♻️ Defensive fix
 function getAttr(attrs: string, name: string): string | undefined {
-  const re = new RegExp(`${name}="([^"]*)"`, 'i')
+  const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+  const re = new RegExp(`${escapedName}="([^"]*)"`, 'i')
   const m = attrs.match(re)
   return m ? m[1] : undefined
 }
packages/generators/src/drawio/generate-drawio.ts (1)

121-129: Non-transitive sort comparator.

The comparator returns 0 for many unrelated pairs, which means the sort result may vary across engines. For parent-before-child ordering, consider a topological sort or at minimum ensure transitivity. In practice this likely works because the input is already partially ordered, but it's fragile.

apps/playground/src/components/drawio/DrawioContextMenu.tsx (1)

76-82: Fragile viewmodel shim with type assertion.

The ad-hoc object { $view: diagram, get $styles() { ... } } cast via as Parameters<typeof generateDrawio>[0] will silently break if generateDrawio's signature evolves (e.g., if it starts accessing .bounds or other LikeC4ViewModel members). Consider creating a shared helper or factory function that produces a properly typed viewmodel shim, reusable across both this component and the Provider.

apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx (1)

121-130: Potential stale closure in window event handlers.

handleImport and handleExport are in the dependency array, so the effect re-subscribes when they change. However, handleExport captures diagram and likec4model from the snapshot — if these change frequently, the event listeners will be torn down and re-added on each render cycle. This is functionally correct but creates some churn. A useRef-based approach for the handlers would avoid repeated listener registration.

Comment on lines +25 to +146
export function DrawioContextMenu({
children,
diagram,
likec4model,
}: DrawioContextMenuProps) {
const playground = usePlayground()
const [opened, { open, close }] = useDisclosure(false)
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 })
const fileInputRef = useRef<HTMLInputElement>(null)

const handleCanvasContextMenu = useCallback((event: React.MouseEvent) => {
event.preventDefault()
setMenuPosition({ x: event.clientX, y: event.clientY })
open()
}, [open])

const handleImport = useCallback(() => {
close()
fileInputRef.current?.click()
}, [close])

const handleImportFile = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
e.target.value = ''
const reader = new FileReader()
reader.onload = () => {
const xml = reader.result as string
try {
const likec4Source = parseDrawioToLikeC4(xml)
const base = file.name.replace(/\.drawio(\.xml)?$/i, '')
const filename = `${base}.c4`
playground.actor.send({
type: 'workspace.addFile',
filename,
content: likec4Source,
})
} catch (err) {
console.error('DrawIO import failed', err)
}
}
reader.readAsText(file, 'utf-8')
},
[playground],
)

const handleExport = useCallback(() => {
close()
if (!diagram) return
try {
const viewmodel = {
$view: diagram,
get $styles() {
return likec4model?.$styles ?? null
},
}
const xml = generateDrawio(viewmodel as Parameters<typeof generateDrawio>[0])
const blob = new Blob([xml], { type: 'application/x-drawio' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${diagram.id}.drawio`
a.click()
URL.revokeObjectURL(url)
} catch (err) {
console.error('DrawIO export failed', err)
}
}, [close, diagram, likec4model])

return (
<>
<input
ref={fileInputRef}
type="file"
accept={DRAWIO_ACCEPT}
style={{ display: 'none' }}
onChange={handleImportFile}
aria-hidden
/>
<Menu
opened={opened}
onClose={close}
position="bottom-start"
withArrow
shadow="md"
closeOnItemClick
styles={{
dropdown: {
position: 'fixed',
left: menuPosition.x,
top: menuPosition.y,
},
}}>
<Menu.Target>
<span
style={{
position: 'fixed',
left: menuPosition.x,
top: menuPosition.y,
width: 1,
height: 1,
pointerEvents: 'none',
}}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>DrawIO</Menu.Label>
<Menu.Item leftSection={<IconFileImport size={16} />} onClick={handleImport}>
Import from DrawIO…
</Menu.Item>
<Menu.Item
leftSection={<IconFileExport size={16} />}
onClick={handleExport}
disabled={!diagram}>
Export to DrawIO
</Menu.Item>
</Menu.Dropdown>
</Menu>
{children(handleCanvasContextMenu)}
</>
)
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.

🛠️ Refactor suggestion | 🟠 Major

Significant duplication with DrawioContextMenuProvider.tsx.

The import flow (FileReader → parseDrawioToLikeC4workspace.addFile), export flow (generateDrawio → Blob download), and the Mantine Menu UI are nearly identical between this component and DrawioContextMenuProvider. Consider extracting the shared import/export logic into a common hook (e.g., useDrawioActions) and the menu UI into a shared presentational component. This would reduce the maintenance surface and prevent divergence.

🤖 Prompt for AI Agents
In `@apps/playground/src/components/drawio/DrawioContextMenu.tsx` around lines 25
- 146, This duplicates import/export and menu UI between DrawioContextMenu and
DrawioContextMenuProvider; extract the shared logic into a new hook (e.g.,
useDrawioActions) that encapsulates fileInputRef, handleImportFile (FileReader →
parseDrawioToLikeC4 → playground.actor.send workspace.addFile) and handleExport
(generateDrawio → Blob + download) and expose import/export helpers and a
trigger ref, then create a presentational component (e.g.,
DrawioContextMenuView) that renders the Mantine Menu (uses props for opened,
onClose, menuPosition, onImport, onExport, disabled state) and reuse it from
both DrawioContextMenu and DrawioContextMenuProvider to eliminate duplication.

Comment on lines +159 to +161
const userData = desc !== '' || tech !== ''
? `\n <mxUserObject><data key="likec4Description">${desc}</data><data key="likec4Technology">${tech}</data></mxUserObject>`
: ''
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

Non-standard <mxUserObject> placement inside <mxCell>.

Standard DrawIO files use <mxUserObject> as a wrapper around <mxCell>, not as a child element inside it. While the round-trip with the companion parser (parse-drawio.ts) works because it scans the inner content of <mxCell> for <data> tags, the resulting .drawio files may lose custom metadata when edited and re-saved in draw.io's own editor (which expects the standard wrapper pattern).

🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/generate-drawio.ts` around lines 159 - 161,
The generated DrawIO XML places <mxUserObject> inside <mxCell> via the userData
string, which is non-standard; change the writer in generate-drawio.ts so that
when desc or tech exist you wrap the entire <mxCell> element with an outer
<mxUserObject> containing the two <data key="likec4Description"> and <data
key="likec4Technology"> tags instead of injecting them as children; update the
code that builds the cell string (reference the userData variable) to emit the
opening <mxUserObject> before the <mxCell> and the closing </mxUserObject> after
it, ensuring proper nesting and that parse-drawio.ts (which scans <mxCell> inner
content) still reads the metadata or adjust parsing accordingly.

return `<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="LikeC4" modified="${new Date().toISOString()}" agent="LikeC4" version="1.0" etag="" type="device">
<diagram name="${escapeXml(view.id)}" id="likec4-${escapeXml(view.id)}">
<mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale=1 pageWidth="827" pageHeight="1169" math="0" shadow="0">
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 | 🟠 Major

Invalid XML: pageScale=1 is missing attribute value quotes.

All other attributes in this element are properly quoted, but pageScale=1 omits them. This produces malformed XML that may cause issues in strict XML parsers.

🐛 Proposed fix
-    <mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale=1 pageWidth="827" pageHeight="1169" math="0" shadow="0">
+    <mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
📝 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
<mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale=1 pageWidth="827" pageHeight="1169" math="0" shadow="0">
<mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/generate-drawio.ts` at line 209, Locate the
XML string in packages/generators/src/drawio/generate-drawio.ts that contains
the '<mxGraphModel ... pageScale=1 ...>' element (the XML fragment used to
generate the draw.io document) and fix the malformed attribute by changing
pageScale=1 to pageScale="1" so all attributes are properly quoted; update any
tests or string builders that construct this element (e.g., the drawio template
or the function that emits the mxGraphModel fragment) to ensure the attribute
gets emitted with quotes.

Comment on lines +95 to +96
const fillColor = styleMap.get('fillcolor') ?? styleMap.get('fillColor')
const strokeColor = styleMap.get('strokecolor') ?? styleMap.get('strokeColor')
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

Dead fallback: styleMap.get('fillColor') will never match.

Line 49 lowercases all keys (k.toLowerCase()), so fillColor is stored as fillcolor. The ?? styleMap.get('fillColor') fallback is dead code. Same applies to strokeColor on line 96.

🔧 Proposed fix
-    const fillColor = styleMap.get('fillcolor') ?? styleMap.get('fillColor')
-    const strokeColor = styleMap.get('strokecolor') ?? styleMap.get('strokeColor')
+    const fillColor = styleMap.get('fillcolor')
+    const strokeColor = styleMap.get('strokecolor')
📝 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
const fillColor = styleMap.get('fillcolor') ?? styleMap.get('fillColor')
const strokeColor = styleMap.get('strokecolor') ?? styleMap.get('strokeColor')
const fillColor = styleMap.get('fillcolor')
const strokeColor = styleMap.get('strokecolor')
🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/parse-drawio.ts` around lines 95 - 96, The
fallbacks styleMap.get('fillColor') and styleMap.get('strokeColor') are dead
because keys are lowercased when populating styleMap (k.toLowerCase()); update
the code so the lookups are consistent: either remove the dead fallbacks and
only use styleMap.get('fillcolor') / styleMap.get('strokecolor') when assigning
the fillColor and strokeColor variables, or stop lowercasing keys at the map
population (remove k.toLowerCase()) if you intended to preserve original casing;
adjust the code near the styleMap population and the fillColor/strokeColor
assignments accordingly.

Comment on lines +132 to +138
function inferKind(style: string | undefined): 'actor' | 'system' | 'container' | 'component' {
if (!style) return 'container'
const s = style.toLowerCase()
if (s.includes('umlactor') || s.includes('shape=person')) return 'actor'
if (s.includes('swimlane') || s.includes('shape=rectangle') && s.includes('rounded')) return 'system'
return 'container'
}
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

Operator precedence makes this condition ambiguous.

&& binds tighter than ||, so this reads as:
s.includes('swimlane') || (s.includes('shape=rectangle') && s.includes('rounded'))

If that's the intent, add explicit parentheses for clarity. If swimlane should also require rounded, the logic is wrong.

🔧 Add explicit parentheses
-  if (s.includes('swimlane') || s.includes('shape=rectangle') && s.includes('rounded')) return 'system'
+  if (s.includes('swimlane') || (s.includes('shape=rectangle') && s.includes('rounded'))) return 'system'
📝 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
function inferKind(style: string | undefined): 'actor' | 'system' | 'container' | 'component' {
if (!style) return 'container'
const s = style.toLowerCase()
if (s.includes('umlactor') || s.includes('shape=person')) return 'actor'
if (s.includes('swimlane') || s.includes('shape=rectangle') && s.includes('rounded')) return 'system'
return 'container'
}
function inferKind(style: string | undefined): 'actor' | 'system' | 'container' | 'component' {
if (!style) return 'container'
const s = style.toLowerCase()
if (s.includes('umlactor') || s.includes('shape=person')) return 'actor'
if (s.includes('swimlane') || (s.includes('shape=rectangle') && s.includes('rounded'))) return 'system'
return 'container'
}
🤖 Prompt for AI Agents
In `@packages/generators/src/drawio/parse-drawio.ts` around lines 132 - 138, In
inferKind, make the ambiguous boolean expression explicit: if the intended rule
is "swimlane OR (shape=rectangle AND rounded)" change the condition to if
(s.includes('swimlane') || (s.includes('shape=rectangle') &&
s.includes('rounded'))) return 'system'; if instead "swimlane" should also
require rounded change it to if ((s.includes('swimlane') &&
s.includes('rounded')) || (s.includes('shape=rectangle') &&
s.includes('rounded'))) return 'system'. Use the inferKind function and the
s.includes(...) checks to locate and update the condition accordingly.

const timer = startTimer(logger)
const likec4 = await LikeC4.fromWorkspace(args.path, {
logger,
graphviz: useDotBin ? 'binary' : 'wasm',
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the handler file
find . -name "handler.ts" -path "*/drawio/*" | head -5

Repository: likec4/likec4

Length of output: 158


🏁 Script executed:

# Locate the options file to understand useDotBin
find . -name "options*" -path "*/cli/*" | head -5

Repository: likec4/likec4

Length of output: 93


🏁 Script executed:

# Check the handler file around line 45
cat -n packages/likec4/src/cli/export/drawio/handler.ts | head -80

Repository: likec4/likec4

Length of output: 3369


🏁 Script executed:

# Now examine the options.ts file to see what useDotBin actually is
cat -n packages/likec4/src/cli/options.ts

Repository: likec4/likec4

Length of output: 3279


🏁 Script executed:

# Also check how args['use-dot'] or args.useDotBin would be accessed in similar handlers
rg -n "args\[.use-dot.\]|args\.useDotBin|args\.useD" packages/likec4/src/cli --type ts

Repository: likec4/likec4

Length of output: 1470


🏁 Script executed:

# Verify the exact option name used in yargs - check the alias
cat -n packages/likec4/src/cli/options.ts | grep -A 8 "export const useDotBin"

Repository: likec4/likec4

Length of output: 406


🏁 Script executed:

# Double-check the codegen handler to see if it's using the wrong pattern too
cat -n packages/likec4/src/cli/codegen/index.ts | sed -n '35,50p'

Repository: likec4/likec4

Length of output: 699


🏁 Script executed:

# Confirm how yargs converts option names to args properties
# Check if 'use-dot' becomes args['use-dot'] or args.useDot
rg -n "option\('use-dot'" packages/likec4/src/cli -A 3 | head -30

Repository: likec4/likec4

Length of output: 2063


🏁 Script executed:

# Check what the actual property name would be when accessing the parsed args
# by looking at serve/index.ts which we know uses args['use-dot']
cat -n packages/likec4/src/cli/serve/index.ts | sed -n '30,45p'

Repository: likec4/likec4

Length of output: 709


Fix: useDotBin is the option descriptor, not the parsed argument value.

Line 45 uses useDotBin (the imported yargs option configuration object, always truthy) instead of the resolved boolean argument. This causes graphviz to always be 'binary' regardless of user input. Use args['use-dot'] instead.

Proposed fix
-      graphviz: useDotBin ? 'binary' : 'wasm',
+      graphviz: args['use-dot'] ? 'binary' : 'wasm',
📝 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
graphviz: useDotBin ? 'binary' : 'wasm',
graphviz: args['use-dot'] ? 'binary' : 'wasm',
🤖 Prompt for AI Agents
In `@packages/likec4/src/cli/export/drawio/handler.ts` at line 45, The graphviz
config is using the option descriptor useDotBin (always truthy) instead of the
parsed CLI value, causing graphviz to be set to 'binary' unconditionally; update
the graphviz assignment in handler.ts to read the resolved argument
args['use-dot'] (or equivalent parsed boolean) rather than useDotBin so that
graphviz becomes 'binary' when args['use-dot'] is true and 'wasm' when false.

@@ -0,0 +1,59 @@
import { parseDrawioToLikeC4 } from '@likec4/generators'
import { readFile, writeFile } from 'node:fs/promises'
import { dirname, extname, relative, resolve } from 'node:path'
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

Unused import: dirname.

dirname is imported but never used in this file.

🧹 Proposed fix
-import { dirname, extname, relative, resolve } from 'node:path'
+import { extname, relative, resolve } from 'node:path'
📝 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
import { dirname, extname, relative, resolve } from 'node:path'
import { extname, relative, resolve } from 'node:path'
🤖 Prompt for AI Agents
In `@packages/likec4/src/cli/import/drawio/handler.ts` at line 3, The import list
in handler.ts includes an unused symbol dirname; remove dirname from the named
import from 'node:path' so the statement imports only used symbols (extname,
relative, resolve) to eliminate the unused-import warning and keep imports
minimal.

Comment on lines +45 to +53
let outfile = args.outfile
if (!outfile) {
const base = inputPath.replace(/\.drawio(\.xml)?$/i, '')
outfile = base + '.c4'
} else if (extname(outfile) !== '.c4') {
outfile = outfile + '.c4'
}

await writeFile(outfile, likec4Source)
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 | 🟠 Major

Missing mkdir -p for the output file's parent directory.

If the user specifies -o some/nested/dir/model.c4 and the intermediate directories don't exist, writeFile will throw ENOENT. The export handler (export/drawio/handler.ts) correctly calls mkdir(outdir, { recursive: true }) before writing. This handler should do the same for the output file's parent.

🐛 Proposed fix
+import { mkdir, readFile, writeFile } from 'node:fs/promises'
+import { dirname, extname, relative, resolve } from 'node:path'
 ...
+      await mkdir(dirname(outfile), { recursive: true })
       await writeFile(outfile, likec4Source)

This also makes use of the currently-unused dirname import.

🤖 Prompt for AI Agents
In `@packages/likec4/src/cli/import/drawio/handler.ts` around lines 45 - 53, The
handler currently computes outfile and calls writeFile(outfile, likec4Source)
without ensuring the parent directory exists, which can cause ENOENT for nested
paths; update the code after computing outfile (variables: outfile, inputPath,
extname) to compute the parent directory via dirname(outfile) and call
mkdir(parentDir, { recursive: true }) before writeFile so the intermediate
directories are created (use the existing dirname import and the writeFile/mkdir
functions).

Copy link
Copy Markdown
Member

@davydkov davydkov left a comment

Choose a reason for hiding this comment

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

Hey @sraphaz this is AWESOME!!

I will merge it and apply all suggested comments in next PR, I will ask to review that

Comment on lines +126 to +129
execSync('pnpm panda ship --outfile ./panda.buildinfo.json', {
stdio: 'inherit',
cwd: process.cwd(),
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just curious, you did it because zx doet not work on windows or you need synchronous operation?

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