refactor: improve type safety in Mermaid integration

- Replace all `any` types with proper Excalidraw types
- Add proper type imports from @excalidraw/excalidraw
- Improve MermaidConversionResult interface with ExcalidrawElement[] and BinaryFiles
- Add mermaidDiagram and config fields to WebSocketMessage interface
- Remove unused ElementBinding interface
- Remove all `as any` type casts throughout App.tsx
- Fix Vite security vulnerability (1 of 6 moderate vulns)

Changes:
- frontend/src/App.tsx: Restored proper TypeScript types, removed 12+ `as any` casts
- frontend/src/utils/mermaidConverter.ts: Added proper return types
- package-lock.json: Updated vite to fix security issue

Remaining security issues:
- 5 moderate vulnerabilities from @excalidraw/mermaid-to-excalidraw dependencies
- These are upstream issues in dompurify, nanoid, and mermaid packages
- No fixes available without major version upgrades
- Risk is acceptable for this use case (diagram rendering)
This commit is contained in:
yctimlin
2025-11-08 00:34:25 +08:00
parent 27a7c65b55
commit 381bc98bff
3 changed files with 40 additions and 39 deletions
+33 -34
View File
@@ -1,14 +1,16 @@
import React, { useState, useEffect, useRef } from 'react'
import {
Excalidraw,
convertToExcalidrawElements,
CaptureUpdateAction
import {
Excalidraw,
convertToExcalidrawElements,
CaptureUpdateAction,
ExcalidrawImperativeAPI
} from '@excalidraw/excalidraw'
import type { ExcalidrawElement, NonDeleted, NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { convertMermaidToExcalidraw, DEFAULT_MERMAID_CONFIG } from './utils/mermaidConverter'
import type { MermaidConfig } from '@excalidraw/mermaid-to-excalidraw'
// Type definitions
type ExcalidrawAPIRefValue = any;
type ExcalidrawElement = any;
type ExcalidrawAPIRefValue = ExcalidrawImperativeAPI;
interface ServerElement {
id: string;
@@ -47,6 +49,8 @@ interface WebSocketMessage {
count?: number;
timestamp?: string;
source?: string;
mermaidDiagram?: string;
config?: MermaidConfig;
}
interface ApiResponse {
@@ -58,11 +62,6 @@ interface ApiResponse {
message?: string;
}
interface ElementBinding {
id: string;
type: 'text' | 'arrow';
}
type SyncStatus = 'idle' | 'syncing' | 'success' | 'error';
// Helper function to clean elements for Excalidraw
@@ -165,7 +164,7 @@ function App(): JSX.Element {
if (result.success && result.elements && result.elements.length > 0) {
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
const convertedElements = convertToExcalidrawElements(cleanedElements as any, { regenerateIds: false })
const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false })
excalidrawAPI?.updateScene({ elements: convertedElements })
}
} catch (error) {
@@ -229,18 +228,18 @@ function App(): JSX.Element {
if (data.elements && data.elements.length > 0) {
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
const validatedElements = validateAndFixBindings(cleanedElements)
const convertedElements = convertToExcalidrawElements(validatedElements as any)
excalidrawAPI.updateScene({
const convertedElements = convertToExcalidrawElements(validatedElements)
excalidrawAPI.updateScene({
elements: convertedElements,
captureUpdate: CaptureUpdateAction.NEVER
})
}
break
case 'element_created':
if (data.element) {
const cleanedNewElement = cleanElementForExcalidraw(data.element)
const newElement = convertToExcalidrawElements([cleanedNewElement] as any)
const newElement = convertToExcalidrawElements([cleanedNewElement])
const updatedElementsAfterCreate = [...currentElements, ...newElement]
excalidrawAPI.updateScene({
elements: updatedElementsAfterCreate,
@@ -252,31 +251,31 @@ function App(): JSX.Element {
case 'element_updated':
if (data.element) {
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement] as any)[0]
const updatedElements = currentElements.map((el: any) =>
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0]
const updatedElements = currentElements.map(el =>
el.id === data.element!.id ? convertedUpdatedElement : el
)
excalidrawAPI.updateScene({
excalidrawAPI.updateScene({
elements: updatedElements,
captureUpdate: CaptureUpdateAction.NEVER
})
}
break
case 'element_deleted':
if (data.elementId) {
const filteredElements = currentElements.filter((el: any) => el.id !== data.elementId)
excalidrawAPI.updateScene({
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
excalidrawAPI.updateScene({
elements: filteredElements,
captureUpdate: CaptureUpdateAction.NEVER
})
}
break
case 'elements_batch_created':
if (data.elements) {
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
const batchElements = convertToExcalidrawElements(cleanedBatchElements as any)
const batchElements = convertToExcalidrawElements(cleanedBatchElements)
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
excalidrawAPI.updateScene({
elements: updatedElementsAfterBatch,
@@ -296,28 +295,28 @@ function App(): JSX.Element {
case 'mermaid_convert':
console.log('Received Mermaid conversion request from MCP')
if ((data as any).mermaidDiagram) {
if (data.mermaidDiagram) {
try {
const result = await convertMermaidToExcalidraw((data as any).mermaidDiagram, (data as any).config || DEFAULT_MERMAID_CONFIG)
const result = await convertMermaidToExcalidraw(data.mermaidDiagram, data.config || DEFAULT_MERMAID_CONFIG)
if (result.error) {
console.error('Mermaid conversion error:', result.error)
return
}
if (result.elements && result.elements.length > 0) {
const convertedElements = convertToExcalidrawElements(result.elements as any, { regenerateIds: false })
excalidrawAPI.updateScene({
const convertedElements = convertToExcalidrawElements(result.elements, { regenerateIds: false })
excalidrawAPI.updateScene({
elements: convertedElements,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
})
if (result.files) {
excalidrawAPI.addFiles(Object.values(result.files))
}
console.log('Mermaid diagram converted successfully:', result.elements.length, 'elements')
// Sync to backend automatically after creating elements
await syncToBackend()
}
@@ -367,7 +366,7 @@ function App(): JSX.Element {
console.log(`Syncing ${currentElements.length} elements to backend`)
// Filter out deleted elements
const activeElements = currentElements.filter((el: any) => !el.isDeleted)
const activeElements = currentElements.filter(el => !el.isDeleted)
// 3. Convert to backend format
const backendElements = activeElements.map(convertToBackendFormat)
+4 -2
View File
@@ -1,8 +1,10 @@
import { parseMermaidToExcalidraw, MermaidConfig } from '@excalidraw/mermaid-to-excalidraw';
import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
import type { BinaryFiles } from '@excalidraw/excalidraw/types/types';
export interface MermaidConversionResult {
elements: any[];
files?: any;
elements: readonly ExcalidrawElement[];
files?: BinaryFiles;
error?: string;
}
+3 -3
View File
@@ -7321,9 +7321,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {