Skip to content

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io (reapply + code scanning)#21

Merged
sraphaz merged 9 commits intomainfrom
feat/drawio-export
Feb 14, 2026
Merged

feat(cli,playground,docs,generators): Export LikeC4 views to Draw.io (reapply + code scanning)#21
sraphaz merged 9 commits intomainfrom
feat/drawio-export

Conversation

@sraphaz
Copy link
Copy Markdown
Owner

@sraphaz sraphaz commented Feb 14, 2026

Reaplica a feature de export Draw.io após revert no origin. Inclui as correções de code scanning (ReDoS em parse-drawio, sanitização em icons). PR para rodar CI no fork.

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Draw.io roundtrip export with layout and style preservation via --roundtrip flag
    • Added --all-in-one option to combine multiple views into a single .drawio file
    • Added --uncompressed export option for improved compatibility
    • Added "Export all views" functionality in Playground Draw.io menu
  • Documentation

    • New Draw.io tooling guide with export options and round-trip usage
    • Updated CLI reference with export flags and examples
  • Tests

    • Added end-to-end tests for Draw.io integration in Playground

… Draw.io" (likec4#2629)

This reverts commit 654240a, reversing
changes made to fdd46ff.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive Draw.io round-trip support with layout/color/waypoint preservation, adds cross-platform postpack infrastructure, removes the CLI import command, refactors Playground DrawIO export flows with multi-view generation, expands e2e testing, updates GitHub Actions runners, and adds Docker-based CI validation scripts.

Changes

Cohort / File(s) Summary
Draw.io Round-trip & Export Infrastructure
.changeset/drawio-*, packages/generators/src/drawio/constants.ts, packages/generators/src/drawio/xml-utils.ts, packages/generators/src/drawio/generate-drawio.ts, packages/generators/src/drawio/parse-drawio.ts, packages/generators/src/drawio/index.ts
Added new Draw.io constants module, centralized XML escaping/decoding utilities, rewrote generateDrawio with round-trip options (layoutOverride, strokeColorByNodeId, strokeWidthByNodeId), added generateDrawioMulti for multi-view export, expanded parse-drawio with decompression and multi-diagram support, updated index exports.
Draw.io CLI Export Handler
packages/likec4/src/cli/export/drawio/handler.ts, packages/likec4/src/cli/export/index.ts
Refactored to support all-in-one and per-view exports with round-trip source discovery; added workspace-wide .c4/.likec4 file reading and per-view option assembly; introduced runExportDrawio orchestrator and per-view error handling without aborting other views.
Draw.io Playground Context Menu Refactor
apps/playground/src/components/drawio/DrawioContextMenu.tsx, apps/playground/src/components/drawio/DrawioContextMenuDropdown.tsx, apps/playground/src/components/drawio/DrawioContextMenuProvider.tsx, apps/playground/src/components/drawio/drawio-events.ts, apps/playground/src/components/drawio/useDrawioContextMenuActions.ts
Replaced legacy DrawioContextMenuView with DrawioContextMenuDropdown; added useDrawioContextMenuActions hook with multi-phase view collection for export-all; introduced LayoutedModelApi for LSP layout integration; removed import-related code and legacy useDrawioActions; refactored to support getSourceContent prop for round-trip export.
Playground Layout/Model Integration
apps/playground/src/monaco/LanguageClientSync.tsx, apps/playground/src/monaco/MonacoEditor.tsx, apps/playground/src/monaco/index.tsx, apps/playground/src/routes/w.$workspaceId/route.tsx
Added setLayoutedModelApi prop to expose layout model API; integrated LSP layout requests via requestLayoutViewCallback; propagated API through Monaco editor to Drawio context provider; added type exports for LayoutedModelApi.
Postpack Cross-platform Implementation
devops/commands/postpack.ts, devops/likec4ops.ts
New postpack command that copies versioned tarball to package.tgz on both Windows (PowerShell) and Unix platforms; integrated into likec4ops CLI.
Package.json Postpack Updates
packages/*/package.json, styled-system/*/package.json
Replaced shell copy commands with likec4ops postpack across all packages (config, core, diagram, generators, language-server, language-services, layouts, likec4, log, mcp, vite-plugin, preset, styles).
CLI Import Removal
packages/likec4/src/cli/import/index.ts, packages/likec4/src/cli/import/drawio/handler.ts, packages/likec4/src/cli/index.ts
Removed entire import command and drawio handler; added exitWithFailure error handling; refactored logger configuration; removed importCmd from CLI wiring.
PNG & JSON Export Refactoring
packages/likec4/src/cli/export/png/handler.ts, packages/likec4/src/cli/export/json/handler.ts
Extracted workflows into runExportPng and runExportJson helpers; updated argument types (PngExportArgs, JsonExportArgs); added per-project filtering and improved error handling; updated flag names (--output to --outdir).
E2E Testing Infrastructure
e2e/playwright.playground.config.ts, e2e/playwright.config.ts, e2e/package.json, e2e/.gitignore, e2e/helpers/selectors.ts, e2e/helpers/timeouts.ts, e2e/E2E-COVERAGE.md
Added Playwright Playground config for DrawIO tests; configured test ignores and screenshot tolerance; added shared selectors/timeout constants; documented e2e coverage design principles.
New E2E Test Suites
e2e/tests/drawio-playground.spec.ts, e2e/tests/static-navigation.spec.ts, e2e/tests/docs-smoke.spec.ts, e2e/src/likec4-cli-export-drawio.spec.ts
Added comprehensive Playground DrawIO context menu tests (export single/all views, file downloads); added static site navigation tests; added docs smoke tests (homepage, Draw.io page, CLI page); added CLI export drawio tests with mxfile validation.
LikeC4 Integration Tests
packages/likec4/src/drawio-demo-export-import.spec.ts, packages/likec4/src/drawio-tutorial-export-import.spec.ts, packages/likec4/src/drawio-test-utils.ts
Added integration tests for DrawIO export/import roundtrips using cloud-system and tutorial models; validated XML structure, element counts, and loadability; added test utilities for cell counting and nested array detection.
GitHub Actions & CI Infrastructure
.github/workflows/ci-pr.yaml, .github/workflows/*.yaml, scripts/ci-validate-in-docker.sh, scripts/ci-validate-in-docker.ps1, vitest.config.ts
Added new ci-pr workflow for fork PRs; updated all runners to ubuntu-24.04; added Docker-based CI validation with PowerShell/Bash scripts; added Vitest exclude patterns.
Documentation Updates
apps/docs/src/content/docs/tooling/drawio.mdx, apps/docs/src/content/docs/tooling/cli.mdx, apps/docs/src/content/docs/tooling/docker.mdx, apps/playground/README.md
New Draw.io tooling documentation covering export concepts, round-trip options, container mapping; updated CLI docs with --roundtrip, --all-in-one, --uncompressed flags and --outdir alias; added Playground troubleshooting section.
Configuration & Utilities
packages/generators/src/index.ts, packages/log/src/formatters.ts, packages/log/src/index.ts, packages/log/src/utils.ts, packages/log/src/sink.ts, packages/vscode/src/utils.ts, packages/vscode/src/node/mcp-server.ts, packages/language-server/src/logger.ts, packages/config/src/filenames.ts
Consolidated drawio exports in generators barrel; enhanced log formatters with getMessageOnlyFormatter; refactored logger config to handle sinks; added loggable utility docs; expanded workspace URI normalization; added readEnvVar for workspace environment variable parsing; enhanced config filename predicates for non-JSON variants.
Language Server Enhancements
packages/language-server/src/browser/worker.ts, packages/language-server/src/documentation/documentation-provider.ts, packages/language-server/src/lsp/HoverProvider.ts, packages/language-server/src/model/model-locator.ts, packages/language-server/src/model/last-seen-artifacts.ts, packages/language-server/src/protocol.ts
Added error stringification and global error handlers in browser worker; refactored documentation provider with exhaustiveness checks; added JSDoc for hover methods; introduced locateElement/locateView APIs; expanded model caching to include styles; added ViewLocateResult type.
Minor Updates
.gitignore, BEGINNER_NONTES.md, .cursor/rules/git-push-remotes.mdc, packages/diagram/vite.config.ts, packages/vscode-preview/vite.config.ts, packages/vscode/src/useWorkspaceId.ts, packages/vscode/src/node/useMcpRegistration.ts, packages/vscode/tsdown.config.ts, packages/vscode-plugin/src/virtuals/icons.ts, packages/core/src/compute-view/.../stage-exclude.spec.ts, packages/diagram/src/.../NotationPanel.tsx, packages/likec4/src/cli/codegen/webcomponent/handler.ts, apps/playground/scripts/generate.mts
Extended gitignore patterns; removed beginner notes file; added git push remotes rule; prioritized TS extensions in Vite resolvers; removed workspace state persistence; improved error handling in MCP registration; added project ID sanitization for icons registry; refactored logging calls and Windows PowerShell support in scripts.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Playground as Playground UI
    participant LSP as Language Server (Layout)
    participant Generator as DrawIO Generator
    participant Exporter as DrawIO Exporter

    User->>Playground: Right-click canvas for context menu
    activate Playground
    Playground->>Playground: useDrawioContextMenuActions hook
    
    alt Export Single View
        Playground->>LSP: Request layout (optional)
        activate LSP
        LSP-->>Playground: layoutedModel / layout data
        deactivate LSP
        
        Playground->>Generator: toViewModel + buildOptions
        activate Generator
        Generator-->>Playground: DrawioViewModelLike
        deactivate Generator
        
        Playground->>Exporter: generateDrawio(viewModel, options)
        activate Exporter
        Exporter-->>Playground: XML string
        deactivate Exporter
        
    else Export All Views
        Playground->>LSP: collectViewModelsForExportAll
        activate LSP
        LSP->>LSP: fillFromLayoutedModel phase
        LSP->>LSP: fillFromViewStates phase
        LSP->>LSP: fillFromLayoutViews phase
        LSP->>LSP: fillFromModelView phase
        LSP-->>Playground: DrawioViewModelLike[]
        deactivate LSP
        
        Playground->>Exporter: generateDrawioMulti(viewModels, optionsByViewId)
        activate Exporter
        Exporter-->>Playground: Multi-tab XML
        deactivate Exporter
    end
    
    Playground->>Exporter: downloadDrawioBlob(xml, filename)
    Exporter-->>User: Download .drawio file
    deactivate Playground
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Oh what a tale of diagrams and flows,
With round-trip data and render-to-shows,
The drawio dreams now cross-platform soar,
While postpacks hop on Windows once more! 🎨
From playground to CLI, the exports take flight,
A massive refactor done oh-so-right! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is minimal and lacks alignment with the provided template. Required checklist items and documentation details are absent; only a brief statement about reapplying features and code-scanning fixes is provided without addressing the template sections. Complete the PR description by filling out the checklist, confirming tests were run, and detailing which documentation was updated (e.g., CLI docs, Draw.io docs page).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: exporting LikeC4 views to Draw.io with code-scanning fixes, indicating a feature reapplication after a revert.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/drawio-export

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (2)
packages/vscode/src/node/mcp-server.ts (1)

40-49: ⚠️ Potential issue | 🟡 Minor

Inconsistent URI handling between JSON-parsed and fallback paths.

Line 43 uses URI.parse(v) which expects URI-formatted strings (e.g., file:///home/user), while line 49 uses URI.file(trimmed) which correctly handles plain filesystem paths. If users provide plain paths in JSON format (e.g., ["/home/user/workspace"]), URI.parse will misinterpret them.

Consider using consistent handling:

🔧 Proposed fix for consistent URI handling
     const parsed = envSchema.parse(JSON.parse(value))
     const asArray = isString(parsed) ? [parsed] : parsed
     if (hasAtLeast(asArray, 1)) {
-      return map(asArray, v => URI.parse(v).fsPath)
+      return map(asArray, v => {
+        // Handle both URI strings (file://...) and plain paths
+        return v.startsWith('file://') ? URI.parse(v).fsPath : URI.file(v).fsPath
+      })
     }

Alternatively, update the JSDoc to explicitly document that JSON values must be URI strings (e.g., file:///path/to/workspace).

packages/likec4/src/cli/export/png/takeScreenshot.ts (1)

141-148: ⚠️ Potential issue | 🔴 Critical

Fix Playwright API usage: locator.evaluate() options parameter position.

The call passes the options object as the second parameter, but the correct Playwright signature for Node.js/TypeScript is locator.evaluate(pageFunction, arg?, options?). The timeout option must be the third parameter:

locator.evaluate<unknown, HTMLImageElement>(
  image =>
    image.complete || new Promise(resolve => {
      image.onload = resolve
      image.onerror = resolve
    }),
  undefined,
  { timeout: Math.max(15_000, timeout) }
)

Currently, the timeout option may not be applied as intended.

🤖 Fix all issues with AI agents
In `@apps/playground/src/monaco/index.tsx`:
- Around line 11-16: Add a JSDoc block above the exported function MonacoEditor
describing the component purpose, its parameter (props: MonacoEditorProps) and
the return type (JSX.Element or React.ReactElement), documenting any important
props or behavior (e.g., uses Suspense and LazyMonacoEditor, fallback Loader).
Keep it concise and follow existing JSDoc style in the repo.
- Around line 1-5: The import ordering is wrong: move the type-only import for
MonacoEditorProps before the value imports (i.e., place "import type {
MonacoEditorProps } from './MonacoEditor'" above the imports for Loader, lazy,
Suspense) so type-only imports are grouped first; keep the "export type {
MonacoEditorProps } from './MonacoEditor'" line as-is (or directly after the
grouped type import) to satisfy the linter.

In `@apps/playground/src/monaco/LanguageClientSync.tsx`:
- Around line 43-51: Add a JSDoc comment for the exported function
LanguageClientSync describing its purpose and public API: document the function
behavior and each parameter (config: CustomWrapperConfig, wrapper:
MonacoEditorLanguageClientWrapper, setLayoutedModelApi?: (api: LayoutedModelApi
| null) => void), include the expected return type (React component) and any
side effects (e.g., setting layouted model API or initializing language client),
and tag it as exported/ public; keep the comment concise and follow existing
JSDoc style used in the codebase.

In `@apps/playground/src/monaco/MonacoEditor.tsx`:
- Around line 1-3: The import grouping is incorrect: move the type-only
import(s) before value imports and ensure they use the TypeScript `import type`
form; specifically, place `import type { LayoutedModelApi } from
'$components/drawio/DrawioContextMenuProvider'` above the value imports (`import
{ usePlayground } from '$/hooks/usePlayground'` and `import { loggable,
rootLogger } from '@likec4/log'`) so all type-only imports are grouped first and
then the runtime/value imports follow, then run the linter to verify ordering.
- Around line 18-20: Add a JSDoc block above the default-exported React
component LazyMonacoEditor describing its purpose, public props
(MonacoEditorProps), and behavior (e.g., it memoizes the editor, accepts
setLayoutedModelApi prop and uses usePlayground). Include param tags for the
props and a `@returns` tag describing the rendered React element, and mark it as
the public component for the Monaco editor.

In `@e2e/helpers/selectors.ts`:
- Around line 6-17: The exported helper functions canvas and editor lack
explicit return types; update the file to import the Locator type (e.g., add
"Locator" to the existing import type from '@playwright/test') and declare
explicit return types on the functions (canvas(page: Page): Locator and
editor(page: Page): Locator) so both helpers return a Locator type instead of
relying on inference.

In `@e2e/tests/drawio-playground.spec.ts`:
- Around line 1-4: The import ordering is incorrect: move the type-only import
"Page" (import type { Page } from '@playwright/test') above the value imports
and group all type-only imports first, then the runtime/value imports (expect,
test, readFile, canvas, CANVAS_SELECTOR, editor, MENU_SELECTOR); update the top
of the file so the "import type { Page }" line appears before the other imports
and keep the remaining imports (expect, test, readFile, and the selectors
canvas/CANVAS_SELECTOR/editor/MENU_SELECTOR) in their original order.

In `@e2e/tests/static-navigation.spec.ts`:
- Around line 1-4: The imports at the top are not grouped with type-only imports
first; move the type-only import "import type { Page } from '@playwright/test'"
before the value imports (e.g., "import { expect, test } from
'@playwright/test'", "import { canvas } from '../helpers/selectors'", "import {
TIMEOUT_CANVAS } from '../helpers/timeouts'") so that all type-only imports are
grouped and appear before runtime/value imports to satisfy the linter.

In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 1054-1074: The code parses metadataJson into metaObj but the
type-check relies on "metaObj && typeof metaObj === 'object' &&
!Array.isArray(metaObj)"; change this to an explicit null/undefined guard (e.g.,
metaObj != null && typeof metaObj === 'object' && !Array.isArray(metaObj')) so
null is unambiguously excluded before iterating its entries; update the
conditional around the JSON.parse result in parse-drawio.ts (the block that
builds metaAttrs and pushes into bodyLines using escapeLikec4Quotes) to use the
explicit != null check.

In `@packages/icons/package.json`:
- Line 77: The change added/edited the "postpack" script entry in the icons
package's package.json (the "postpack" key); revert that edit so the generated
icons package remains untouched, or if this change was intentional, add an
explicit review approval note and update the generation script instead of
manually editing the package.json; locate the "postpack" key in the icons
package's package.json and either restore its original value from main branch or
document/approve the generator change that produces this file.

In `@packages/language-server/src/browser/worker.ts`:
- Around line 8-21: Refactor the if/else chain inside the errToString function
to use a switch(true) pattern while preserving all existing branch behavior and
order: test err instanceof Error, typeof === 'object' with null check and the
JSON.stringify try/catch fallback, typeof === 'string', null/undefined check,
typeof === 'number' or 'boolean', typeof === 'symbol' (call toString), and the
final default 'unknown' return; ensure no logic or return values change and keep
the function signature errToString(err: unknown): string unchanged.

In `@packages/language-server/src/documentation/documentation-provider.ts`:
- Around line 16-19: Add a JSDoc comment for the public override method
getDocumentation in the DocumentationProvider class: describe the method's
purpose, its parameters (types/meanings) and the return value, and include any
thrown errors or important behavior (e.g., async/Promise or cancellation
support) so it matches the existing JSDoc style used for the class and
constructor; target the getDocumentation method in DocumentationProvider and
ensure the comment is placed immediately above the method declaration and uses
the same JSDoc conventions as the file.

In `@packages/likec4/src/logger.ts`:
- Around line 17-20: Add a JSDoc comment block above the public debug method to
document its purpose and usage: describe that Logger.debug logs a debug-level
message, list parameters (msg: string, ...args: unknown[]) with types and note
that args is a variadic list of context values, and state the return type
(void). Reference the method name debug and the internal call to logger.debug in
the description so readers know this wraps the underlying logger; include a
short example or note about how args are attached (e.g., passed as an args
object) if helpful.

In `@packages/vscode/tsdown.config.ts`:
- Around line 109-113: The async function copyPreview lacks an explicit return
type; update its signature to declare a Promise<void> return type (i.e., change
function copyPreview() to function copyPreview(): Promise<void>) so TypeScript
explicit-types guideline is followed; keep the implementation unchanged and
ensure any callers still work with the Promise<void> signature.
🧹 Nitpick comments (19)
packages/vscode/src/utils.ts (1)

3-38: Add explicit return types to exported functions.

Guidelines require explicit types; these exports currently rely on inference. Please annotate return types (and consider a named type for performanceMark for clarity).

✏️ Proposed fix
-export function now() {
+export function now(): number {
   try {
     return performance.now()
   } catch {
     return Date.now()
   }
 }

-export function performanceMark() {
+export type PerformanceMark = {
+  readonly ms: number
+  readonly pretty: string
+}
+
+export function performanceMark(): PerformanceMark {
   const t0 = now()
   return {
     get ms(): number {
       return now() - t0
     },
     get pretty(): string {
       return prettyMs(now() - t0)
     },
   }
 }

-export function isLikeC4Source(path: string) {
+export function isLikeC4Source(path: string): boolean {
   const p = path.toLowerCase()
   return p.endsWith('.c4') || p.endsWith('.likec4') || p.endsWith('.like-c4')
 }
.changeset/drawio-cli-roundtrip-e2e.md (1)

8-8: Minor clarity improvement: Add "files" after file extensions.

For better readability, consider adding the word "files" after the file extensions.

📝 Suggested improvement
-- **CLI:** `likec4 export drawio --roundtrip` reads all `.c4`/`.likec4` in the workspace, parses round-trip comment blocks (layout, stroke colors/widths, edge waypoints), and applies them when generating each view's `.drawio` file.
+- **CLI:** `likec4 export drawio --roundtrip` reads all `.c4`/`.likec4` files in the workspace, parses round-trip comment blocks (layout, stroke colors/widths, edge waypoints), and applies them when generating each view's `.drawio` file.
apps/playground/scripts/generate.mts (1)

5-13: Consider storing platform() result to avoid redundant calls.

The Windows handling logic is correct. Minor nit: platform() is called twice—once on line 6 and again on line 12. While platform() is cheap (returns a cached value), storing the result improves readability.

♻️ Optional: Store platform result in a variable
+const isWindows = platform() === 'win32'
+
 // On Windows, zx needs PowerShell (no bash by default in v8+)
-if (platform() === 'win32') {
+if (isWindows) {
   usePowerShell()
 }
 const $ = _$({
   stdio: 'inherit',
   preferLocal: true,
-  ...(platform() === 'win32' ? { quote: quotePowerShell } : {}),
+  ...(isWindows ? { quote: quotePowerShell } : {}),
 })
packages/vscode/src/node/mcp-server.ts (1)

29-34: JSDoc should clarify expected input format.

The JSDoc mentions "JSON array of paths" but doesn't specify whether these should be URI-formatted strings (file:///...) or plain filesystem paths. Given the inconsistency noted above, clarifying the expected format would help users provide correct input.

📝 Suggested JSDoc improvement
 /**
- * Read LIKEC4_WORKSPACE env: JSON array of paths or single path string.
+ * Read LIKEC4_WORKSPACE env: JSON array of file URIs (e.g., "file:///path") or single path string.
  * Falls back to treating raw value as one workspace path if not valid JSON.
  * `@param` name - Environment variable name (e.g. 'LIKEC4_WORKSPACE').
- * `@returns` Array of absolute fs paths, or undefined if unset/invalid.
+ * `@returns` Non-empty array of absolute fs paths, or undefined if unset/empty.
  */
packages/log/src/index.ts (1)

65-70: Simplify redundant sinks extraction.

Line 65 extracts sinks as _sinks (unused), then line 66 re-extracts it via config?.sinks. Use default value in destructuring instead.

♻️ Suggested simplification
-    const { sinks: _sinks, loggers: _loggers, ...restConfig } = config ?? {}
-    const sinks = config?.sinks ?? {}
+    const { sinks = {}, loggers: _loggers, ...restConfig } = config ?? {}
packages/core/src/compute-view/deployment-view/stages/stage-exclude.spec.ts (1)

60-62: Consider tracking this limitation more formally.

The added comments clearly document why this test is skipped and the underlying limitation. However, since the comment asks to "Decide: fix stage-exclude or adjust expectation," this appears to be an unresolved design question rather than a temporary skip.

Consider:

  • Using it.todo instead of it.skip to make this more visible in test reports as pending work
  • Creating a tracking issue to document the decision needed and ensure it's not forgotten
Alternative: Use it.todo for better visibility
-    // Skip: excluding both ui and api leaves their parent deployment node (e.g. zone1) in final; implementation
-    // does not remove "empty" deployment nodes when all children are excluded. Decide: fix stage-exclude or adjust expectation.
-    it.skip('should remove element and all its connections', () => {
+    // Excluding both ui and api leaves their parent deployment node (e.g. zone1) in final; implementation
+    // does not remove "empty" deployment nodes when all children are excluded. Decide: fix stage-exclude or adjust expectation.
+    it.todo('should remove element and all its connections', () => {

This makes the pending decision more visible in test output.

packages/config/src/filenames.ts (1)

47-62: Type predicate narrows to basename type, not the full path.

The type predicates claim filename is typeof configJsonFilenames[number], but the check is performed on basename(filename). This means if you pass "/path/to/likec4.config.json", the function returns true and TypeScript narrows the type to '.likec4rc' | '.likec4.config.json' | 'likec4.config.json', even though the actual value is a full path.

This is a common pattern and works for most use cases where you just need to know "is this a config file". However, if callers rely on the narrowed type expecting an exact filename, they may be surprised.

Consider whether a simple boolean return is more accurate, or document this behavior:

/** Returns true if the **basename** of the given path matches a known config filename. */
export function isLikeC4JsonConfig(filename: string): boolean {
  return (configJsonFilenames as readonly string[]).includes(basename(filename))
}
packages/language-server/src/lsp/HoverProvider.ts (4)

83-85: Good addition of documentation.

The JSDoc comment accurately describes the method's purpose. Consider enhancing it with @param and @returns tags for more comprehensive documentation:

/**
 * Builds hover content for an element node (title, summary, model details and view links).
 * `@param` node - The element AST node to generate hover content for
 * `@returns` Hover information with markdown content, or undefined if element not found
 */

This would provide more context for developers working with this class.


115-117: Documentation added effectively.

The comment clearly explains the method's functionality. For completeness, consider adding parameter and return type documentation:

/**
 * Builds markdown hover content for an element model (relationship counts and links to views).
 * `@param` model - The element model containing relationship and view information
 * `@param` projectId - The project identifier for constructing view links
 * `@returns` Markdown string with model details, or undefined if no details available
 */

167-169: Documentation improves code clarity.

The JSDoc comment appropriately describes the method. As with the other methods, consider using structured JSDoc tags:

/**
 * Builds hover content for a deployment node (id, title, kind, summary).
 * `@param` node - The deployment node AST node to generate hover content for
 * `@returns` Hover information with markdown content
 */

192-194: Documentation enhances maintainability.

The comment accurately captures the method's behavior. For consistency with JSDoc best practices, consider the structured format:

/**
 * Builds hover content for a deployed instance (instance id, element ref, title and kind).
 * `@param` node - The deployed instance AST node to generate hover content for
 * `@returns` Hover information with markdown content showing instance details
 */
packages/likec4/src/cli/export/json/handler.ts (1)

71-72: Extension check is case-sensitive.

The check extname(outfile) !== '.json' won't match uppercase variants like .JSON. This is a minor edge case but could cause files like output.JSON.json.

♻️ Proposed fix for case-insensitive check
- if (extname(outfile) !== '.json') outfile = outfile + '.json'
+ if (extname(outfile).toLowerCase() !== '.json') outfile = outfile + '.json'
packages/likec4/src/cli/export/png/handler.ts (1)

346-354: Inconsistent property access for yargs args could cause undefined values.

The handler mixes kebab-case and camelCase property access:

  • Line 336: args['max-attempts'] (kebab-case)
  • Line 346: args.maxAttempts (camelCase)
  • Line 349: args.serverUrl (camelCase) for option 'server-url'

While yargs performs camelCase conversion by default, this inconsistency makes the code harder to maintain and could break if yargs configuration changes. Consider using consistent kebab-case access throughout to match the option definitions.

♻️ Proposed fix for consistent property access
       await pngHandler(
         {
           path: args.path,
           useDotBin: args['use-dot'],
           output: args.outdir,
           project: args.project,
           timeoutMs: args.timeout * 1000,
-          maxAttempts: args.maxAttempts,
+          maxAttempts: args['max-attempts'],
           ignore: args.ignore === true,
           outputType: args.flat ? 'flat' : 'relative',
-          serverUrl: args.serverUrl,
+          serverUrl: args['server-url'],
           theme,
           filter: args.filter,
           sequence: args.seq,
           chromiumSandbox: args['chromium-sandbox'],
         } satisfies PngExportArgs,
       )
packages/language-server/src/documentation/documentation-provider.ts (1)

71-72: Consider refactoring the if-chain to switch(true).
This aligns the block with the preferred control-flow style.

♻️ Suggested refactor
-      if (ast.isDeploymentNode(node)) {
-        const el = parser.parseDeploymentNode(node)
-        return `**${el.title}**`
-      }
-
-      if (ast.isDeployedInstance(node)) {
-        const instance = parser.parseDeployedInstance(node)
-        const [projectId, fqn] = FqnRef.isImportRef(instance.element)
-          ? [instance.element.project as ProjectId, instance.element.model as Fqn]
-          : [doc.likec4ProjectId, instance.element.model as Fqn]
-        const found = projectId ? this.locator.getParsedElement(fqn, projectId) : this.locator.getParsedElement(fqn)
-        const lines = [
-          `_instance of_ \`${fqn}\``,
-        ]
-        if (found) {
-          lines.push(`**${found.element.title}**`)
-        }
-        return lines.join('  \n')
-      }
-
-      if (ast.isElement(node)) {
-        const el = parser.parseElement(node)
-        if (!el) {
-          return
-        }
-        const lines = [
-          `**${el.title}**`,
-          `<small>kind: \`${el.kind}\`</small>`,
-        ]
-        return lines.join('  \n')
-      }
-
-      // Exhaustiveness check — errors at compile time if guard admits a new node type without a handler
-      const _exhaustive: never = node
+      switch (true) {
+        case ast.isDeploymentNode(node): {
+          const el = parser.parseDeploymentNode(node)
+          return `**${el.title}**`
+        }
+        case ast.isDeployedInstance(node): {
+          const instance = parser.parseDeployedInstance(node)
+          const [projectId, fqn] = FqnRef.isImportRef(instance.element)
+            ? [instance.element.project as ProjectId, instance.element.model as Fqn]
+            : [doc.likec4ProjectId, instance.element.model as Fqn]
+          const found = projectId ? this.locator.getParsedElement(fqn, projectId) : this.locator.getParsedElement(fqn)
+          const lines = [
+            `_instance of_ \`${fqn}\``,
+          ]
+          if (found) {
+            lines.push(`**${found.element.title}**`)
+          }
+          return lines.join('  \n')
+        }
+        case ast.isElement(node): {
+          const el = parser.parseElement(node)
+          if (!el) {
+            return
+          }
+          const lines = [
+            `**${el.title}**`,
+            `<small>kind: \`${el.kind}\`</small>`,
+          ]
+          return lines.join('  \n')
+        }
+        default: {
+          // Exhaustiveness check — errors at compile time if guard admits a new node type without a handler
+          const _exhaustive: never = node
+          return _exhaustive
+        }
+      }

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

packages/likec4/src/cli/export/png/takeScreenshot.ts (1)

23-27: Consider refining the JSDoc for accuracy.

The added JSDoc follows the coding guideline requirement but could be more precise:

  1. The @param params documents a parameter that doesn't exist in the signature (the function uses destructuring).
  2. The @returns description mentions "or failed after maxAttempts" but the function always resolves—it returns only the successful captures as an array of { view, path } objects.

Based on learnings, use JSDoc to document public classes and methods.

📝 Suggested refinement
 /**
  * Capture each view as PNG under the output directory using the provided browser context.
- * `@param` params - Browser context, views, output path, logger, timeout, and options (outputType, dynamicVariant, theme).
- * `@returns` Promise resolving when all views are captured (or failed after maxAttempts).
+ * `@param` params - Configuration object containing browser context, views, output path, logger, timeout, and rendering options.
+ * `@returns` Promise resolving to an array of successfully captured views with their file paths.
  */

Alternatively, document each destructured property individually:

 /**
  * Capture each view as PNG under the output directory using the provided browser context.
- * `@param` params - Browser context, views, output path, logger, timeout, and options (outputType, dynamicVariant, theme).
- * `@returns` Promise resolving when all views are captured (or failed after maxAttempts).
+ * `@param` params.browserContext - The Playwright browser context to use for rendering
+ * `@param` params.views - Non-empty array of diagram views to capture
+ * `@param` params.output - Output directory path for PNG files
+ * `@param` params.logger - Logger instance for progress and error reporting
+ * `@param` params.timeout - Timeout in milliseconds for page operations
+ * `@param` params.maxAttempts - Maximum number of retry attempts per view
+ * `@param` params.dynamicVariant - Optional variant for dynamic views (e.g., 'sequence')
+ * `@param` params.outputType - Output path strategy: 'relative' preserves source structure, 'flat' outputs all to root
+ * `@param` params.theme - Theme to use for rendering ('light' or 'dark')
+ * `@returns` Promise resolving to an array of successfully captured views with their output file paths
  */
packages/likec4/src/cli/index.ts (1)

111-115: Minor redundancy in exit handling.

Setting process.exitCode = 1 before exit(1) is redundant since exit(1) explicitly uses the provided code. However, this is harmless and may be intentional for defensive coding.

♻️ Optional simplification
 function exitWithFailure(err: unknown, prefix?: string): never {
-  process.exitCode = 1
   console.error(prefix != null ? `${prefix} ${loggable(err)}` : loggable(err))
   exit(1)
 }
packages/likec4/src/cli/export/drawio/handler.ts (1)

136-144: Consider type-safe view ID extraction.

The cast vm.$view.id as string bypasses type safety. If the view ID type is already string, the cast is redundant; if it's a branded type (e.g., ViewId), the cast could hide type mismatches downstream.

Suggested approach

If the type system guarantees vm.$view.id is string-compatible, consider using String(vm.$view.id) for explicit conversion, or verify the underlying type to remove the cast entirely.

e2e/E2E-COVERAGE.md (1)

1-32: Consider translating documentation to English for consistency.

The E2E coverage documentation is written in Portuguese while the rest of the codebase (code comments, other docs, commit messages) appears to use English. For consistency and broader team accessibility, consider translating this to English.

e2e/src/likec4-cli-export-drawio.spec.ts (1)

23-37: Document the skip reason in the test description or add a tracking issue.

The test is skipped with a detailed comment explaining the CI issue, but this information is only visible when reading the source. Consider:

  1. Including a link to a tracking issue if one exists
  2. Adding the skip reason to the test name for visibility in test reports
Example
-test.skip(
-  'LikeC4 CLI - export drawio produces .drawio file with mxfile',
+test.skip(
+  'LikeC4 CLI - export drawio produces .drawio file with mxfile (SKIPPED: CI tgz path resolution issue)',

Or create a GitHub issue and reference it:

// TODO(`#123`): Re-enable when CI tgz path resolution is fixed
test.skip(

sraphaz and others added 8 commits February 14, 2026 11:50
…ctors/specs, parse-drawio metaObj, icons revert postpack, worker switch(true), docs JSDoc, logger debug, tsdown copyPreview, vscode utils/types, log destructure, stage-exclude it.todo, config filenames JSDoc, HoverProvider @param/@returns, json/png/drawio handlers, E2E-COVERAGE en, test skip reason)

Co-authored-by: Cursor <[email protected]>
…uild picking JSX-in-js); ignore diagram/src/**/*.js

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/config/src/filenames.ts (1)

52-60: ⚠️ Potential issue | 🟠 Major

Type predicates are unsound when full paths are accepted.

isLikeC4NonJsonConfig and isLikeC4Config use type predicates that narrow the input string to specific filename literals, but they accept full paths and extract the basename internally. This creates a type safety hole: if code calls isLikeC4Config('/path/likec4.config.ts'), TypeScript narrows the type to 'likec4.config.ts', which is incorrect. Align both functions with isLikeC4JsonConfig by returning boolean instead of using type predicates.

Suggested fix
-export function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number] {
+export function isLikeC4NonJsonConfig(filename: string): boolean {
   return (configNonJsonFilenames as readonly string[]).includes(basename(filename))
 }
@@
-export function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number] {
+export function isLikeC4Config(filename: string): boolean {
   return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename)
 }
🧹 Nitpick comments (8)
packages/vscode/src/utils.ts (1)

15-33: Consider sharing the performance mark helper to avoid drift.
There’s a very similar performanceMark() in packages/language-server/src/utils/index.ts; if package boundaries allow, centralizing the logic could prevent subtle divergence.

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

164-165: Consider adding the same explanatory comment for consistency.

The bundleConfig uses the same extensions array as defaultConfig, but lacks the comment explaining why. Adding a similar comment (or extracting the array to a shared constant) would improve maintainability.

♻️ Option 1: Add comment
   resolve: {
+    // Prefer .ts/.tsx over .js so build uses source (avoid CJS .js with JSX in src/)
     extensions: ['.ts', '.tsx', '.mts', '.mjs', '.js', '.jsx', '.json'],
♻️ Option 2: Extract to shared constant
+// Prefer .ts/.tsx over .js so build uses source (avoid CJS .js with JSX in src/)
+const resolveExtensions = ['.ts', '.tsx', '.mts', '.mjs', '.js', '.jsx', '.json']
+
 const defaultConfig = defineConfig({
   // ...
   resolve: {
     conditions: ['sources'],
-    // Prefer .ts/.tsx over .js so build uses source (avoid CJS .js with JSX in src/)
-    extensions: ['.ts', '.tsx', '.mts', '.mjs', '.js', '.jsx', '.json'],
+    extensions: resolveExtensions,
     // ...
   },
   // ...
 })

 const bundleConfig = defineConfig({
   // ...
   resolve: {
-    extensions: ['.ts', '.tsx', '.mts', '.mjs', '.js', '.jsx', '.json'],
+    extensions: resolveExtensions,
     // ...
   },
   // ...
 })
packages/likec4/src/logger.ts (1)

66-70: hasErrorLogged should also be omitted for the optional override to take effect.

The JSDoc states the intent is Vite compatibility. Currently, only 'debug' is omitted before re-adding both debug and hasErrorLogged as optional. However, hasErrorLogged from ReturnType (which is required) intersected with the optional version still results in a required property due to TypeScript intersection semantics.

If the intent is for both to be optional:

♻️ Proposed fix
-export type ViteLogger = Omit<ReturnType<typeof createLikeC4Logger>, 'debug'> & {
+export type ViteLogger = Omit<ReturnType<typeof createLikeC4Logger>, 'debug' | 'hasErrorLogged'> & {
   debug?: (msg: string, ...args: unknown[]) => void
   hasErrorLogged?: (error: Error | RollupError) => boolean
 }
apps/playground/src/monaco/LanguageClientSync.tsx (1)

112-144: Consider adding minimal error logging for debugging.

The silent catch blocks at lines 123-124 and 136-137 make debugging difficult if issues arise. While the errors are non-critical for the Draw.io export feature, adding a debug-level log would help during development.

✨ Optional: Add debug logging
           try {
             const { model } = await c.sendRequest(FetchLayoutedModel.req, {})
             return model ?? null
           } catch {
+            if (import.meta.env.DEV) logger.debug`getLayoutedModel request failed`
             return null
           }
             try {
               const res = await requestLayoutView(c, viewId as ViewId)
               if (res.result?.diagram) out[viewId] = res.result.diagram
             } catch {
+              if (import.meta.env.DEV) logger.debug`layoutViews failed for ${viewId}`
               // skip failed view
             }
scripts/ci-validate-in-docker.sh (1)

16-18: Prefer a frozen lockfile for CI reproducibility.

For CI-like validation, consider --frozen-lockfile to avoid drift and ensure deterministic installs.

♻️ Proposed change
-echo "==> Installing dependencies..."
-pnpm install --prefer-offline
+echo "==> Installing dependencies..."
+pnpm install --prefer-offline --frozen-lockfile
scripts/ci-validate-in-docker.ps1 (1)

9-9: Use Write-Output instead of Write-Host for compatibility.

PSScriptAnalyzer flags Write-Host as non-redirectable; Write-Output is safer across hosts.

🔧 Proposed change
-Write-Host "==> Starting Linux container (node:22-bookworm) with workspace: $workspace"
+Write-Output "==> Starting Linux container (node:22-bookworm) with workspace: $workspace"
e2e/tests/drawio-playground.spec.ts (1)

45-50: Consider logging or commenting the intentional catch.

The empty .catch(() => {}) on line 48 silently swallows networkidle timeout errors. While this is a common pattern to avoid flaky tests when network-idle isn't strictly necessary, adding a brief comment would clarify the intent for future maintainers.

💡 Suggested improvement
     await page.goto(TUTORIAL_PATH)
     await page.waitForURL(/\/w\/tutorial\//)
-    await page.waitForLoadState('networkidle').catch(() => {})
+    // networkidle is best-effort; tests can proceed once selector is ready
+    await page.waitForLoadState('networkidle').catch(() => {})
     await page.waitForSelector(CANVAS_SELECTOR, { timeout: TIMEOUT_DIAGRAM })
packages/generators/src/drawio/parse-drawio.ts (1)

671-677: Consider extracting a typed helper for filtered container cells.

The non-null assertions (cont.x!, cont.y!, etc.) are safe because the filter on lines 663-670 guarantees these properties exist. However, TypeScript's type narrowing doesn't propagate through filter(). A minor refinement would be to use a type guard or map to a narrower type.

Optional: Type-safe container cell extraction
+type ContainerCellWithGeometry = DrawioCell & {
+  x: number
+  y: number
+  width: number
+  height: number
+}
+
+function isContainerWithGeometry(v: DrawioCell): v is ContainerCellWithGeometry {
+  return (
+    v.style?.toLowerCase().includes('container=1') === true &&
+    v.x != null &&
+    v.y != null &&
+    v.width != null &&
+    v.height != null
+  )
+}

-  const containerCells = vertices.filter(
-    v =>
-      v.style?.toLowerCase().includes('container=1') &&
-      v.x != null &&
-      v.y != null &&
-      v.width != null &&
-      v.height != null,
-  )
+  const containerCells = vertices.filter(isContainerWithGeometry)
   for (const cont of containerCells) {
-    const cx = cont.x!,
-      cy = cont.y!,
-      cw = cont.width!,
-      ch = cont.height!
+    const cx = cont.x,
+      cy = cont.y,
+      cw = cont.width,
+      ch = cont.height

@sraphaz sraphaz merged commit ce74954 into main Feb 14, 2026
47 of 57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant