feat(playground,cli,docs): DrawIO bidirecional e menu no Playground#2592
Conversation
- 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]>
|
There was a problem hiding this comment.
💡 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"> |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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, '') |
There was a problem hiding this comment.
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 👍 / 👎.
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 withfs.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
unlinkSyncsilently fails on directories), is shorter, and achieves the same cross-platform goal.fs.rmSyncwithrecursive+forceis available since Node 14.14.packages/diagram/vite.config.ts (1)
126-129:execSyncwill throw on non-zero exit — consider wrapping with try/catch for consistency.The
errguard on line 121 gracefully skips on failure, but ifpnpm panda shipitself 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 forexecSyncand 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 reusingindexFilesat line 218 to avoid a duplicate crawl and ensure consistent filtering.Lines 187–192 crawl
**/index.tswith anode_modulesfilter, but line 218 performs the same crawl again without the filter. You could reuseindexFilesin theforloop at line 218 to avoid the redundant filesystem crawl and ensurenode_modulespaths 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 beforeassign, 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 existingworkspace.changeActiveFilepattern (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/DrawioContextMenuProviderand 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 whetherparseDrawioToLikeC4throws or returns a fallback.packages/generators/src/drawio/generate-drawio.spec.ts (1)
48-52:mockViewModelwrapsvi.fnbut the mock is never asserted on.Since you never check call counts or arguments, a plain function would be cleaner. Using
vi.fnhere 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
nameis always a hardcoded string from internal callers, theRegExpconstructor 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
0for 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 viaas Parameters<typeof generateDrawio>[0]will silently break ifgenerateDrawio's signature evolves (e.g., if it starts accessing.boundsor otherLikeC4ViewModelmembers). 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.
handleImportandhandleExportare in the dependency array, so the effect re-subscribes when they change. However,handleExportcapturesdiagramandlikec4modelfrom 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. AuseRef-based approach for the handlers would avoid repeated listener registration.
| 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)} | ||
| </> | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Significant duplication with DrawioContextMenuProvider.tsx.
The import flow (FileReader → parseDrawioToLikeC4 → workspace.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.
| const userData = desc !== '' || tech !== '' | ||
| ? `\n <mxUserObject><data key="likec4Description">${desc}</data><data key="likec4Technology">${tech}</data></mxUserObject>` | ||
| : '' |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
| <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.
| const fillColor = styleMap.get('fillcolor') ?? styleMap.get('fillColor') | ||
| const strokeColor = styleMap.get('strokecolor') ?? styleMap.get('strokeColor') |
There was a problem hiding this comment.
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.
| 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.
| 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' | ||
| } |
There was a problem hiding this comment.
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.
| 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', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the handler file
find . -name "handler.ts" -path "*/drawio/*" | head -5Repository: likec4/likec4
Length of output: 158
🏁 Script executed:
# Locate the options file to understand useDotBin
find . -name "options*" -path "*/cli/*" | head -5Repository: 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 -80Repository: 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.tsRepository: 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 tsRepository: 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 -30Repository: 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.
| 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' | |||
There was a problem hiding this comment.
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.
| 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.
| 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) |
There was a problem hiding this comment.
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).
| execSync('pnpm panda ship --outfile ./panda.buildinfo.json', { | ||
| stdio: 'inherit', | ||
| cwd: process.cwd(), | ||
| }) |
There was a problem hiding this comment.
Just curious, you did it because zx doet not work on windows or you need synchronous operation?
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)
likec4 export drawio [path] -o <outdir>— gera um.drawiopor view; preserva posições, hierarquia, cores, descrição e tecnologia.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: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
.drawioem LikeC4 e adiciona como novo arquivo no workspace (ex.:imported.c4)..drawioe inicia download.editor.addAction()comcontextMenuGroupId: '9_cutcopypaste'.DrawioContextMenuProviderenvolve o layout do workspace; expõeopenMenu(event)via contexto React; o diagrama usaonCanvasContextMenue o painel do editor usaonContextMenuno wrapper. Constantes de evento emdrawio-events.tspara o Monaco não importar o provider (evita dependência pesada no bundler).4. Playground: suporte a novo arquivo (Import)
workspace.addFilena máquina do playground:{ filename, content }— atualizafiles,originalFiles,activeFilenamee persiste.activeFilenamemuda para um arquivo ainda não aberto (ex.: novo.c4do import),LanguageClientSyncchamaensureFileInWorkspace(), cria o modelo no editor, enviaBuildDocumentsao LSP erequestComputedModel()para o diagrama atualizar.5. Dependências e configuração Playground
@likec4/generatorsadicionado ao playground (paragenerateDrawioeparseDrawioToLikeC4).miniflareadicionado como dependência explícita do playground para o@cloudflare/vite-pluginno dev (evita "Expected miniflare to be defined").strip-vscode-textmate-sourcemappara remover//# sourceMappingURLdo vscode-textmate na pré-bundlização e reduzir aviso de source map no console.6. Arquivos principais alterados/criados
packages/generators(drawio), `packages/likec4/src/cli/exportapps/docs/.../tooling/cli.mdx,packages/likec4/README.mdapps/playground/src/components/drawio/(DrawioContextMenuProvider, drawio-events, DrawioContextMenu),routes/w.$workspaceId/route.tsx,$viewId.tsxapps/playground/src/state/playground-machine.ts(workspace.addFile)apps/playground/src/monaco/LanguageClientSync.tsx(ações DrawIO + ensureFile + BuildDocuments),utils.ts(ensureFileInWorkspace),config(não alterado para DrawIO)apps/playground/vite.config.ts(plugin sourcemap),apps/playground/package.json(generators, miniflare)Checklist (template do repo)
packages/generators(generate-drawio.spec.ts, parse-drawio.spec.ts); testes do playground são manuais (menu e import/export).Como testar
pnpm install(raiz),pnpm devemapps/playground. Abrir um workspace (ex.: tutorial)..drawio→ novo arquivo.c4deve aparecer no editor e o diagrama atualizar.<viewId>.drawio.likec4 export drawio -o ./out,likec4 import drawio file.drawio -o model.c4(requer Graphviz para export com layout).Checklist (template oficial do repo)
mainbefore creating this PR.Summary by CodeRabbit
New Features
Documentation