Creating first version of MCP server.
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled
This commit is contained in:
265
src/utils/file-manager.ts
Normal file
265
src/utils/file-manager.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { DiagramFormat, DiagramType } from '../types/diagram-types.js';
|
||||
|
||||
// Functional types
|
||||
type FileConfig = Readonly<{
|
||||
workspaceRoot: string;
|
||||
}>;
|
||||
|
||||
type DiagramFileMetadata = Readonly<{
|
||||
path: string;
|
||||
relativePath: string;
|
||||
format: DiagramFormat;
|
||||
isDiagram: boolean;
|
||||
stats: fs.Stats;
|
||||
}>;
|
||||
|
||||
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
|
||||
|
||||
// Pure function to create file configuration
|
||||
export const createFileConfig = (workspaceRoot?: string): FileConfig => ({
|
||||
workspaceRoot: workspaceRoot || process.cwd()
|
||||
});
|
||||
|
||||
// Pure function to get file extension mapping
|
||||
const getFileExtensionMap = (): FileExtensionMap => ({
|
||||
[DiagramFormat.DRAWIO]: 'drawio',
|
||||
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
|
||||
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
|
||||
[DiagramFormat.DIO]: 'dio',
|
||||
[DiagramFormat.XML]: 'xml'
|
||||
});
|
||||
|
||||
// Pure function to get file extension for diagram format
|
||||
export const getFileExtension = (format: DiagramFormat): string => {
|
||||
const extensionMap = getFileExtensionMap();
|
||||
return extensionMap[format] || 'drawio';
|
||||
};
|
||||
|
||||
// Pure function to get diagram format from file path
|
||||
export const getDiagramFormat = (filePath: string): DiagramFormat => {
|
||||
const fileName = path.basename(filePath).toLowerCase();
|
||||
|
||||
if (fileName.endsWith('.drawio.svg')) {
|
||||
return DiagramFormat.DRAWIO_SVG;
|
||||
} else if (fileName.endsWith('.drawio.png')) {
|
||||
return DiagramFormat.DRAWIO_PNG;
|
||||
} else if (fileName.endsWith('.drawio')) {
|
||||
return DiagramFormat.DRAWIO;
|
||||
} else if (fileName.endsWith('.dio')) {
|
||||
return DiagramFormat.DIO;
|
||||
} else if (fileName.endsWith('.xml')) {
|
||||
return DiagramFormat.XML;
|
||||
}
|
||||
|
||||
return DiagramFormat.DRAWIO;
|
||||
};
|
||||
|
||||
// Pure function to get search patterns for diagram files
|
||||
const getDiagramSearchPatterns = (): readonly string[] => [
|
||||
'**/*.drawio',
|
||||
'**/*.drawio.svg',
|
||||
'**/*.drawio.png',
|
||||
'**/*.dio',
|
||||
'**/*.xml'
|
||||
];
|
||||
|
||||
// Pure function to get ignore patterns
|
||||
const getIgnorePatterns = (): readonly string[] => [
|
||||
'node_modules/**',
|
||||
'.git/**',
|
||||
'build/**',
|
||||
'dist/**'
|
||||
];
|
||||
|
||||
// Curried function to find diagram files
|
||||
export const findDiagramFiles = (config: FileConfig) => async (): Promise<readonly string[]> => {
|
||||
const patterns = getDiagramSearchPatterns();
|
||||
const ignorePatterns = getIgnorePatterns();
|
||||
|
||||
const files: string[] = [];
|
||||
for (const pattern of patterns) {
|
||||
const matches = await glob(pattern, {
|
||||
cwd: config.workspaceRoot,
|
||||
ignore: [...ignorePatterns]
|
||||
});
|
||||
files.push(...matches);
|
||||
}
|
||||
|
||||
return files.map(file => path.resolve(config.workspaceRoot, file));
|
||||
};
|
||||
|
||||
// Pure function to create full file name with extension
|
||||
const createFullFileName = (fileName: string, format: DiagramFormat): string => {
|
||||
const extension = getFileExtension(format);
|
||||
return fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
|
||||
};
|
||||
|
||||
// Curried function to create diagram file
|
||||
export const createDiagramFile = (config: FileConfig) =>
|
||||
(fileName: string) =>
|
||||
(content: string) =>
|
||||
(format: DiagramFormat) =>
|
||||
async (outputDir?: string): Promise<string> => {
|
||||
const dir = outputDir || config.workspaceRoot;
|
||||
const fullFileName = createFullFileName(fileName, format);
|
||||
const filePath = path.join(dir, fullFileName);
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
|
||||
// Write file
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Higher-order function for file operations with error handling
|
||||
const withFileErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`File operation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to read diagram file content
|
||||
export const readDiagramFile = withFileErrorHandling(async (filePath: string): Promise<string> => {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
return await fs.readFile(filePath, 'utf8');
|
||||
});
|
||||
|
||||
// Pure function to update existing diagram file
|
||||
export const updateDiagramFile = withFileErrorHandling(async (filePath: string, content: string): Promise<void> => {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
});
|
||||
|
||||
// Curried function to convert diagram format
|
||||
export const convertDiagramFormat = (config: FileConfig) =>
|
||||
(sourcePath: string) =>
|
||||
(targetFormat: DiagramFormat) =>
|
||||
async (targetPath?: string): Promise<string> => {
|
||||
const content = await readDiagramFile(sourcePath);
|
||||
const sourceDir = path.dirname(sourcePath);
|
||||
const baseName = path.basename(sourcePath, path.extname(sourcePath));
|
||||
|
||||
const newExtension = getFileExtension(targetFormat);
|
||||
const newFileName = targetPath || path.join(sourceDir, `${baseName}.${newExtension}`);
|
||||
|
||||
const createFile = createDiagramFile(config);
|
||||
await createFile(path.basename(newFileName))(content)(targetFormat)(path.dirname(newFileName));
|
||||
|
||||
return newFileName;
|
||||
};
|
||||
|
||||
// Pure function to check if file is a diagram file
|
||||
export const isDiagramFile = async (filePath: string): Promise<boolean> => {
|
||||
const fileName = path.basename(filePath).toLowerCase();
|
||||
const isDiagramExtension = fileName.endsWith('.drawio') ||
|
||||
fileName.endsWith('.drawio.svg') ||
|
||||
fileName.endsWith('.drawio.png') ||
|
||||
fileName.endsWith('.dio');
|
||||
|
||||
if (isDiagramExtension) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileName.endsWith('.xml')) {
|
||||
return await isDrawioXml(filePath);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Pure function to check if XML file is a draw.io diagram
|
||||
export const isDrawioXml = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return content.includes('mxGraphModel') || content.includes('mxfile');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to ensure workspace directory exists
|
||||
export const ensureWorkspaceDir = (config: FileConfig) => async (subDir?: string): Promise<string> => {
|
||||
const targetDir = subDir ? path.join(config.workspaceRoot, subDir) : config.workspaceRoot;
|
||||
await fs.ensureDir(targetDir);
|
||||
return targetDir;
|
||||
};
|
||||
|
||||
// Curried function to get relative path from workspace root
|
||||
export const getRelativePath = (config: FileConfig) => (filePath: string): string => {
|
||||
return path.relative(config.workspaceRoot, filePath);
|
||||
};
|
||||
|
||||
// Higher-order function to create file metadata
|
||||
const createFileMetadata = (config: FileConfig) => async (filePath: string): Promise<DiagramFileMetadata> => ({
|
||||
path: filePath,
|
||||
relativePath: getRelativePath(config)(filePath),
|
||||
format: getDiagramFormat(filePath),
|
||||
isDiagram: await isDiagramFile(filePath),
|
||||
stats: await fs.stat(filePath)
|
||||
});
|
||||
|
||||
// Curried function to get all diagram files with their metadata
|
||||
export const getDiagramFilesWithMetadata = (config: FileConfig) => async (): Promise<readonly DiagramFileMetadata[]> => {
|
||||
const files = await findDiagramFiles(config)();
|
||||
const createMetadata = createFileMetadata(config);
|
||||
|
||||
return Promise.all(files.map(createMetadata));
|
||||
};
|
||||
|
||||
// Curried function to create validated file configuration
|
||||
export const createValidatedFileConfig = async (workspaceRoot?: string): Promise<FileConfig> => {
|
||||
const config = createFileConfig(workspaceRoot);
|
||||
|
||||
// Ensure workspace exists
|
||||
await ensureWorkspaceDir(config)();
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for file operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Functional file operations with retry
|
||||
export const readDiagramFileWithRetry = withRetry(readDiagramFile);
|
||||
export const updateDiagramFileWithRetry = withRetry(updateDiagramFile);
|
366
src/utils/vscode-integration.ts
Normal file
366
src/utils/vscode-integration.ts
Normal file
@ -0,0 +1,366 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { DiagramFormat } from '../types/diagram-types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Functional types
|
||||
type VSCodeConfig = Readonly<{
|
||||
workspaceRoot: string;
|
||||
extensionId: string;
|
||||
}>;
|
||||
|
||||
type ExtensionStatus = Readonly<{
|
||||
isInstalled: boolean;
|
||||
isVSCodeAvailable: boolean;
|
||||
version?: string;
|
||||
}>;
|
||||
|
||||
type SetupResult = Readonly<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>;
|
||||
|
||||
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
|
||||
|
||||
type PlatformCommandMap = Readonly<Record<string, string>>;
|
||||
|
||||
// Pure function to create VSCode configuration
|
||||
export const createVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => ({
|
||||
workspaceRoot: workspaceRoot || process.cwd(),
|
||||
extensionId: 'hediet.vscode-drawio'
|
||||
});
|
||||
|
||||
// Pure function to get file extension mapping
|
||||
const getFileExtensionMap = (): FileExtensionMap => ({
|
||||
[DiagramFormat.DRAWIO]: 'drawio',
|
||||
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
|
||||
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
|
||||
[DiagramFormat.DIO]: 'dio',
|
||||
[DiagramFormat.XML]: 'xml'
|
||||
});
|
||||
|
||||
// Pure function to get file extension for diagram format
|
||||
export const getFileExtension = (format: DiagramFormat): string => {
|
||||
const extensionMap = getFileExtensionMap();
|
||||
return extensionMap[format] || 'drawio';
|
||||
};
|
||||
|
||||
// Pure function to get platform-specific file explorer commands
|
||||
const getPlatformCommandMap = (targetPath: string): PlatformCommandMap => ({
|
||||
'win32': `explorer "${targetPath}"`,
|
||||
'darwin': `open "${targetPath}"`,
|
||||
'linux': `xdg-open "${targetPath}"`
|
||||
});
|
||||
|
||||
// Pure function to get platform-specific file explorer command
|
||||
const getFileExplorerCommand = (targetPath: string): string => {
|
||||
const platform = process.platform;
|
||||
const commandMap = getPlatformCommandMap(targetPath);
|
||||
|
||||
const command = commandMap[platform];
|
||||
if (!command) {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
// Higher-order function for command execution with error handling
|
||||
const withCommandErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`Command execution failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to execute VSCode command
|
||||
export const executeVSCodeCommand = withCommandErrorHandling(async (command: string, args?: readonly string[]): Promise<string> => {
|
||||
const fullCommand = args ? `${command} ${args.join(' ')}` : command;
|
||||
const { stdout } = await execAsync(fullCommand);
|
||||
return stdout;
|
||||
});
|
||||
|
||||
// Curried function to open diagram file in VSCode
|
||||
export const openDiagramInVSCode = (config: VSCodeConfig) => async (filePath: string): Promise<void> => {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(config.workspaceRoot, filePath);
|
||||
|
||||
await executeVSCodeCommand('code', [absolutePath]);
|
||||
};
|
||||
|
||||
// Curried function to check if draw.io extension is installed
|
||||
export const isDrawioExtensionInstalled = (config: VSCodeConfig) => async (): Promise<boolean> => {
|
||||
try {
|
||||
const stdout = await executeVSCodeCommand('code', ['--list-extensions']);
|
||||
return stdout.includes(config.extensionId);
|
||||
} catch (error) {
|
||||
console.warn('Could not check VSCode extensions:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to install draw.io extension
|
||||
export const installDrawioExtension = (config: VSCodeConfig) => async (): Promise<void> => {
|
||||
await executeVSCodeCommand('code', ['--install-extension', config.extensionId]);
|
||||
};
|
||||
|
||||
// Curried function to open VSCode workspace
|
||||
export const openWorkspace = (config: VSCodeConfig) => async (): Promise<void> => {
|
||||
await executeVSCodeCommand('code', [config.workspaceRoot]);
|
||||
};
|
||||
|
||||
// Pure function to check if VSCode is available
|
||||
export const isVSCodeAvailable = async (): Promise<boolean> => {
|
||||
try {
|
||||
await executeVSCodeCommand('code', ['--version']);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to get VSCode version
|
||||
export const getVSCodeVersion = async (): Promise<string> => {
|
||||
const stdout = await executeVSCodeCommand('code', ['--version']);
|
||||
return stdout.split('\n')[0];
|
||||
};
|
||||
|
||||
// Curried function to create and open new diagram in VSCode
|
||||
export const createAndOpenDiagram = (config: VSCodeConfig) =>
|
||||
(fileName: string) =>
|
||||
(content: string) =>
|
||||
(format: DiagramFormat) =>
|
||||
async (outputDir?: string): Promise<string> => {
|
||||
const dir = outputDir || config.workspaceRoot;
|
||||
const extension = getFileExtension(format);
|
||||
const fullFileName = fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
|
||||
const filePath = path.join(dir, fullFileName);
|
||||
|
||||
// Write file
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
|
||||
// Open in VSCode
|
||||
await openDiagramInVSCode(config)(filePath);
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Curried function to get workspace folders
|
||||
export const getWorkspaceFolders = (config: VSCodeConfig) => async (): Promise<readonly string[]> => {
|
||||
try {
|
||||
// This would require VSCode API integration
|
||||
// For now, return current workspace
|
||||
return [config.workspaceRoot];
|
||||
} catch (error) {
|
||||
console.warn('Could not get workspace folders:', error);
|
||||
return [config.workspaceRoot];
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to show notification (placeholder for VSCode API)
|
||||
export const showNotification = async (
|
||||
message: string,
|
||||
type: 'info' | 'warning' | 'error' = 'info'
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// This would require VSCode extension API
|
||||
// For now, just log to console
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
} catch (error) {
|
||||
console.warn('Could not show notification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to open file explorer at specific path
|
||||
export const openFileExplorer = (config: VSCodeConfig) => async (dirPath?: string): Promise<void> => {
|
||||
const targetPath = dirPath || config.workspaceRoot;
|
||||
const command = getFileExplorerCommand(targetPath);
|
||||
await executeVSCodeCommand(command);
|
||||
};
|
||||
|
||||
// Pure function to refresh workspace (placeholder for VSCode API)
|
||||
export const refreshWorkspace = async (): Promise<void> => {
|
||||
try {
|
||||
// This would require VSCode API integration
|
||||
// For now, just log
|
||||
console.log('Workspace refresh requested');
|
||||
} catch (error) {
|
||||
console.warn('Could not refresh workspace:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to check VSCode extension status
|
||||
export const checkExtensionStatus = (config: VSCodeConfig) => async (): Promise<ExtensionStatus> => {
|
||||
const isVSCodeAvail = await isVSCodeAvailable();
|
||||
|
||||
if (!isVSCodeAvail) {
|
||||
return {
|
||||
isInstalled: false,
|
||||
isVSCodeAvailable: false
|
||||
};
|
||||
}
|
||||
|
||||
const isInstalled = await isDrawioExtensionInstalled(config)();
|
||||
const version = isVSCodeAvail ? await getVSCodeVersion() : undefined;
|
||||
|
||||
return {
|
||||
isInstalled,
|
||||
isVSCodeAvailable: isVSCodeAvail,
|
||||
version
|
||||
};
|
||||
};
|
||||
|
||||
// Curried function to setup VSCode environment for draw.io
|
||||
export const setupVSCodeEnvironment = (config: VSCodeConfig) => async (): Promise<SetupResult> => {
|
||||
try {
|
||||
const status = await checkExtensionStatus(config)();
|
||||
|
||||
if (!status.isVSCodeAvailable) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'VSCode is not available. Please install VSCode first.'
|
||||
};
|
||||
}
|
||||
|
||||
if (!status.isInstalled) {
|
||||
await installDrawioExtension(config)();
|
||||
return {
|
||||
success: true,
|
||||
message: 'Draw.io extension installed successfully.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'VSCode environment is ready for draw.io.'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to setup VSCode environment: ${error}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to open multiple diagrams in VSCode
|
||||
export const openMultipleDiagrams = (config: VSCodeConfig) => async (filePaths: readonly string[]): Promise<readonly string[]> => {
|
||||
const results: string[] = [];
|
||||
const openDiagram = openDiagramInVSCode(config);
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
await openDiagram(filePath);
|
||||
results.push(`Successfully opened: ${filePath}`);
|
||||
} catch (error) {
|
||||
results.push(`Failed to open ${filePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Pure function to create VSCode workspace configuration for draw.io
|
||||
export const createWorkspaceConfig = (config: VSCodeConfig): Readonly<Record<string, any>> => ({
|
||||
'hediet.vscode-drawio': {
|
||||
'local-storage': path.join(config.workspaceRoot, '.vscode', 'drawio-storage'),
|
||||
'theme': 'automatic',
|
||||
'online-url': 'https://embed.diagrams.net/',
|
||||
'offline': false
|
||||
}
|
||||
});
|
||||
|
||||
// Curried function to save VSCode workspace settings
|
||||
export const saveWorkspaceSettings = (config: VSCodeConfig) => async (settings: Readonly<Record<string, any>>): Promise<void> => {
|
||||
const settingsPath = path.join(config.workspaceRoot, '.vscode', 'settings.json');
|
||||
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(settingsPath));
|
||||
|
||||
let existingSettings = {};
|
||||
if (await fs.pathExists(settingsPath)) {
|
||||
const content = await fs.readFile(settingsPath, 'utf8');
|
||||
existingSettings = JSON.parse(content);
|
||||
}
|
||||
|
||||
const mergedSettings = { ...existingSettings, ...settings };
|
||||
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save workspace settings: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Higher-order function for operations with timeout
|
||||
export const withTimeout = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
timeoutMs: number = 30000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
return Promise.race([
|
||||
operation(...args),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
// Functional VSCode operations with retry and timeout
|
||||
export const openDiagramInVSCodeWithRetry = (config: VSCodeConfig) =>
|
||||
withRetry(openDiagramInVSCode(config));
|
||||
|
||||
export const setupVSCodeEnvironmentWithTimeout = (config: VSCodeConfig) =>
|
||||
withTimeout(setupVSCodeEnvironment(config));
|
||||
|
||||
// Pure function to validate VSCode configuration
|
||||
export const validateVSCodeConfig = (config: VSCodeConfig): boolean => {
|
||||
return typeof config.workspaceRoot === 'string' &&
|
||||
config.workspaceRoot.length > 0 &&
|
||||
typeof config.extensionId === 'string' &&
|
||||
config.extensionId.length > 0;
|
||||
};
|
||||
|
||||
// Pure function to create validated VSCode configuration
|
||||
export const createValidatedVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => {
|
||||
const config = createVSCodeConfig(workspaceRoot);
|
||||
|
||||
if (!validateVSCodeConfig(config)) {
|
||||
throw new Error('Invalid VSCode configuration');
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
476
src/utils/xml-parser.ts
Normal file
476
src/utils/xml-parser.ts
Normal file
@ -0,0 +1,476 @@
|
||||
import * as xml2js from 'xml2js';
|
||||
import { DiagramData, DiagramElement, DiagramConnection, DiagramType, DiagramFormat } from '../types/diagram-types.js';
|
||||
|
||||
// Functional types
|
||||
type ParserConfig = Readonly<{
|
||||
explicitArray: boolean;
|
||||
mergeAttrs: boolean;
|
||||
normalize: boolean;
|
||||
normalizeTags: boolean;
|
||||
trim: boolean;
|
||||
}>;
|
||||
|
||||
type BuilderConfig = Readonly<{
|
||||
xmldec: Readonly<{ version: string; encoding: string }>;
|
||||
renderOpts: Readonly<{ pretty: boolean; indent: string }>;
|
||||
}>;
|
||||
|
||||
type MxGraphModel = Readonly<{
|
||||
mxGraphModel: any;
|
||||
}>;
|
||||
|
||||
type CellData = Readonly<{
|
||||
id: string;
|
||||
value?: string;
|
||||
style?: string;
|
||||
vertex?: string;
|
||||
edge?: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
parent?: string;
|
||||
mxGeometry?: any;
|
||||
}>;
|
||||
|
||||
type StyleMap = Readonly<Record<string, string>>;
|
||||
|
||||
// Pure function to create default parser configuration
|
||||
export const createParserConfig = (): ParserConfig => ({
|
||||
explicitArray: false,
|
||||
mergeAttrs: true,
|
||||
normalize: true,
|
||||
normalizeTags: true,
|
||||
trim: true
|
||||
});
|
||||
|
||||
// Pure function to create default builder configuration
|
||||
export const createBuilderConfig = (): BuilderConfig => ({
|
||||
xmldec: { version: '1.0', encoding: 'UTF-8' },
|
||||
renderOpts: { pretty: true, indent: ' ' }
|
||||
});
|
||||
|
||||
// Pure function to create parser instance
|
||||
export const createParser = (config: ParserConfig = createParserConfig()): xml2js.Parser =>
|
||||
new xml2js.Parser(config);
|
||||
|
||||
// Pure function to create builder instance
|
||||
export const createBuilder = (config: BuilderConfig = createBuilderConfig()): xml2js.Builder =>
|
||||
new xml2js.Builder(config);
|
||||
|
||||
// Pure function to get default style mappings
|
||||
const getDefaultStyleMap = (): StyleMap => ({
|
||||
'bpmn-start-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
'bpmn-end-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;',
|
||||
'bpmn-task': 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
'bpmn-gateway': 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
|
||||
'uml-class': 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;fillColor=#dae8fc;strokeColor=#6c8ebf;',
|
||||
'uml-actor': 'shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#d5e8d4;strokeColor=#82b366;',
|
||||
'er-entity': 'whiteSpace=wrap;html=1;align=center;treeFolding=1;treeMoving=1;newEdgeStyle={"edgeStyle":"entityRelationEdgeStyle","startArrow":"none","endArrow":"none","segment":10,"curved":1};fillColor=#e1d5e7;strokeColor=#9673a6;',
|
||||
'rectangle': 'rounded=0;whiteSpace=wrap;html=1;',
|
||||
'ellipse': 'ellipse;whiteSpace=wrap;html=1;',
|
||||
'diamond': 'rhombus;whiteSpace=wrap;html=1;',
|
||||
'triangle': 'triangle;whiteSpace=wrap;html=1;'
|
||||
});
|
||||
|
||||
// Pure function to get default style for element type
|
||||
export const getDefaultStyle = (elementType: string): string => {
|
||||
const styleMap = getDefaultStyleMap();
|
||||
return styleMap[elementType] || styleMap['rectangle'];
|
||||
};
|
||||
|
||||
// Pure function to get default connection style
|
||||
export const getDefaultConnectionStyle = (): string =>
|
||||
'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;';
|
||||
|
||||
// Pure function to generate unique ID
|
||||
export const generateId = (): string =>
|
||||
Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Pure function to generate etag for draw.io
|
||||
export const generateEtag = (): string =>
|
||||
Math.random().toString(36).substr(2, 16);
|
||||
|
||||
// Higher-order function for XML operations with error handling
|
||||
const withXMLErrorHandling = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>
|
||||
) => async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
throw new Error(`XML operation failed: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Pure function to extract properties from cell
|
||||
const extractCellProperties = (cell: CellData): Readonly<Record<string, any>> => {
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
if (cell.style) properties.style = cell.style;
|
||||
if (cell.value) properties.value = cell.value;
|
||||
if (cell.vertex) properties.vertex = cell.vertex;
|
||||
if (cell.edge) properties.edge = cell.edge;
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
// Pure function to infer element type from cell data
|
||||
const inferElementType = (cell: CellData): string => {
|
||||
const style = cell.style || '';
|
||||
|
||||
// BPMN elements
|
||||
if (style.includes('bpmn')) return 'bpmn-element';
|
||||
if (style.includes('startEvent')) return 'bpmn-start-event';
|
||||
if (style.includes('endEvent')) return 'bpmn-end-event';
|
||||
if (style.includes('task')) return 'bpmn-task';
|
||||
if (style.includes('gateway')) return 'bpmn-gateway';
|
||||
|
||||
// UML elements
|
||||
if (style.includes('uml')) return 'uml-element';
|
||||
if (style.includes('class')) return 'uml-class';
|
||||
if (style.includes('actor')) return 'uml-actor';
|
||||
if (style.includes('usecase')) return 'uml-usecase';
|
||||
|
||||
// Database elements
|
||||
if (style.includes('entity')) return 'er-entity';
|
||||
if (style.includes('table')) return 'db-table';
|
||||
|
||||
// Network elements
|
||||
if (style.includes('router')) return 'network-router';
|
||||
if (style.includes('switch')) return 'network-switch';
|
||||
if (style.includes('server')) return 'network-server';
|
||||
|
||||
// Generic shapes
|
||||
if (style.includes('rectangle')) return 'rectangle';
|
||||
if (style.includes('ellipse')) return 'ellipse';
|
||||
if (style.includes('rhombus')) return 'diamond';
|
||||
if (style.includes('triangle')) return 'triangle';
|
||||
|
||||
return 'generic-shape';
|
||||
};
|
||||
|
||||
// Pure function to create DiagramElement from cell data
|
||||
const createElementFromCell = (cell: CellData): DiagramElement => {
|
||||
const geometry = cell.mxGeometry || {};
|
||||
|
||||
return {
|
||||
id: cell.id,
|
||||
type: inferElementType(cell),
|
||||
label: cell.value || '',
|
||||
style: cell.style || '',
|
||||
geometry: {
|
||||
x: parseFloat(geometry.x) || 0,
|
||||
y: parseFloat(geometry.y) || 0,
|
||||
width: parseFloat(geometry.width) || 100,
|
||||
height: parseFloat(geometry.height) || 50
|
||||
},
|
||||
properties: extractCellProperties(cell)
|
||||
};
|
||||
};
|
||||
|
||||
// Pure function to create DiagramConnection from cell data
|
||||
const createConnectionFromCell = (cell: CellData): DiagramConnection => ({
|
||||
id: cell.id,
|
||||
source: cell.source || '',
|
||||
target: cell.target || '',
|
||||
label: cell.value || '',
|
||||
style: cell.style || '',
|
||||
properties: extractCellProperties(cell)
|
||||
});
|
||||
|
||||
// Pure function to infer diagram type from elements and connections
|
||||
const inferDiagramType = (elements: readonly DiagramElement[], connections: readonly DiagramConnection[]): DiagramType => {
|
||||
const elementTypes = elements.map(e => e.type);
|
||||
|
||||
// Check for BPMN elements
|
||||
if (elementTypes.some(type => type.startsWith('bpmn-'))) {
|
||||
return DiagramType.BPMN_PROCESS;
|
||||
}
|
||||
|
||||
// Check for UML elements
|
||||
if (elementTypes.some(type => type.startsWith('uml-'))) {
|
||||
if (elementTypes.includes('uml-class')) return DiagramType.UML_CLASS;
|
||||
if (elementTypes.includes('uml-actor')) return DiagramType.UML_USE_CASE;
|
||||
return DiagramType.UML_CLASS; // Default UML type
|
||||
}
|
||||
|
||||
// Check for ER elements
|
||||
if (elementTypes.some(type => type.startsWith('er-') || type.startsWith('db-'))) {
|
||||
return DiagramType.ER_DIAGRAM;
|
||||
}
|
||||
|
||||
// Check for network elements
|
||||
if (elementTypes.some(type => type.startsWith('network-'))) {
|
||||
return DiagramType.NETWORK_TOPOLOGY;
|
||||
}
|
||||
|
||||
// Default to flowchart
|
||||
return DiagramType.FLOWCHART;
|
||||
};
|
||||
|
||||
// Pure function to extract elements and connections from mxGraphModel
|
||||
const extractElementsAndConnections = (mxGraphModel: MxGraphModel): Readonly<{ elements: DiagramElement[], connections: DiagramConnection[] }> => {
|
||||
const elements: DiagramElement[] = [];
|
||||
const connections: DiagramConnection[] = [];
|
||||
|
||||
// Parse cells from mxGraphModel
|
||||
const root = mxGraphModel.mxGraphModel.root;
|
||||
const cells = Array.isArray(root.mxCell) ? root.mxCell : [root.mxCell];
|
||||
|
||||
for (const cell of cells) {
|
||||
if (!cell || cell.id === '0' || cell.id === '1') continue; // Skip root cells
|
||||
|
||||
if (cell.edge) {
|
||||
// This is a connection/edge
|
||||
connections.push(createConnectionFromCell(cell));
|
||||
} else {
|
||||
// This is an element/vertex
|
||||
elements.push(createElementFromCell(cell));
|
||||
}
|
||||
}
|
||||
|
||||
return { elements, connections };
|
||||
};
|
||||
|
||||
// Curried function to extract mxGraphModel from parsed XML
|
||||
const extractMxGraphModel = (parser: xml2js.Parser) => async (result: any): Promise<MxGraphModel> => {
|
||||
if (result.mxfile) {
|
||||
// Standard .drawio format
|
||||
const diagram = Array.isArray(result.mxfile.diagram)
|
||||
? result.mxfile.diagram[0]
|
||||
: result.mxfile.diagram;
|
||||
return await parser.parseStringPromise(diagram.mxGraphModel || diagram._);
|
||||
} else if (result.mxGraphModel) {
|
||||
// Direct mxGraphModel format
|
||||
return result;
|
||||
} else {
|
||||
throw new Error('Invalid draw.io XML format');
|
||||
}
|
||||
};
|
||||
|
||||
// Main function to parse draw.io XML content to DiagramData
|
||||
export const parseDrawioXML = withXMLErrorHandling(async (xmlContent: string): Promise<DiagramData> => {
|
||||
const parser = createParser();
|
||||
|
||||
const result = await parser.parseStringPromise(xmlContent);
|
||||
|
||||
// Handle different draw.io XML formats
|
||||
const mxGraphModel = await extractMxGraphModel(parser)(result);
|
||||
const { elements, connections } = extractElementsAndConnections(mxGraphModel);
|
||||
|
||||
return {
|
||||
elements,
|
||||
connections,
|
||||
metadata: {
|
||||
type: inferDiagramType(elements, connections),
|
||||
format: DiagramFormat.DRAWIO,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Pure function to create cell from DiagramElement
|
||||
const createCellFromElement = (element: DiagramElement): CellData => ({
|
||||
id: element.id,
|
||||
value: element.label || '',
|
||||
style: element.style || getDefaultStyle(element.type),
|
||||
vertex: '1',
|
||||
parent: '1',
|
||||
mxGeometry: {
|
||||
x: element.geometry?.x || 0,
|
||||
y: element.geometry?.y || 0,
|
||||
width: element.geometry?.width || 100,
|
||||
height: element.geometry?.height || 50,
|
||||
as: 'geometry'
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create cell from DiagramConnection
|
||||
const createCellFromConnection = (connection: DiagramConnection): CellData => ({
|
||||
id: connection.id,
|
||||
value: connection.label || '',
|
||||
style: connection.style || getDefaultConnectionStyle(),
|
||||
edge: '1',
|
||||
parent: '1',
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
mxGeometry: {
|
||||
relative: '1',
|
||||
as: 'geometry'
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create cells array from diagram data
|
||||
const createCellsFromDiagramData = (diagramData: DiagramData): readonly CellData[] => {
|
||||
const cells: CellData[] = [
|
||||
{ id: '0' },
|
||||
{ id: '1', parent: '0' }
|
||||
];
|
||||
|
||||
// Add elements
|
||||
const elementCells = diagramData.elements.map(createCellFromElement);
|
||||
cells.push(...elementCells);
|
||||
|
||||
// Add connections
|
||||
const connectionCells = diagramData.connections.map(createCellFromConnection);
|
||||
cells.push(...connectionCells);
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
// Pure function to create mxGraphModel structure
|
||||
const createMxGraphModel = (cells: readonly CellData[]): Readonly<Record<string, any>> => ({
|
||||
mxGraphModel: {
|
||||
dx: '1422',
|
||||
dy: '794',
|
||||
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',
|
||||
root: {
|
||||
mxCell: [...cells]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pure function to create mxfile structure
|
||||
const createMxFile = (mxGraphModel: Readonly<Record<string, any>>, builder: xml2js.Builder): Readonly<Record<string, any>> => ({
|
||||
mxfile: {
|
||||
host: 'Electron',
|
||||
modified: new Date().toISOString(),
|
||||
agent: 'Mozilla/5.0',
|
||||
etag: generateEtag(),
|
||||
version: '24.7.17',
|
||||
type: 'device',
|
||||
diagram: {
|
||||
id: generateId(),
|
||||
name: 'Page-1',
|
||||
mxGraphModel: builder.buildObject(mxGraphModel).replace('<?xml version="1.0" encoding="UTF-8"?>', '')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main function to convert DiagramData to draw.io XML format
|
||||
export const generateDrawioXML = (diagramData: DiagramData): string => {
|
||||
const builder = createBuilder();
|
||||
const cells = createCellsFromDiagramData(diagramData);
|
||||
const mxGraphModel = createMxGraphModel(cells);
|
||||
const mxfile = createMxFile(mxGraphModel, builder);
|
||||
|
||||
return builder.buildObject(mxfile);
|
||||
};
|
||||
|
||||
// Pure function to validate XML content
|
||||
export const validateDrawioXML = async (xmlContent: string): Promise<boolean> => {
|
||||
try {
|
||||
await parseDrawioXML(xmlContent);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Curried function to convert between different XML formats
|
||||
export const convertXMLFormat = (targetFormat: DiagramFormat) => async (xmlContent: string): Promise<string> => {
|
||||
const diagramData = await parseDrawioXML(xmlContent);
|
||||
diagramData.metadata = { ...diagramData.metadata, format: targetFormat };
|
||||
return generateDrawioXML(diagramData);
|
||||
};
|
||||
|
||||
// Utility functions for functional composition
|
||||
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduce((acc, fn) => fn(acc), value);
|
||||
|
||||
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), value);
|
||||
|
||||
// Higher-order function for operations with retry logic
|
||||
export const withRetry = <T extends any[], R>(
|
||||
operation: (...args: T) => Promise<R>,
|
||||
maxRetries: number = 3,
|
||||
delay: number = 1000
|
||||
) => async (...args: T): Promise<R> => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
|
||||
// Functional XML operations with retry
|
||||
export const parseDrawioXMLWithRetry = withRetry(parseDrawioXML);
|
||||
export const validateDrawioXMLWithRetry = withRetry(validateDrawioXML);
|
||||
|
||||
// Pure function to create diagram metadata
|
||||
export const createDiagramMetadata = (
|
||||
type: DiagramType,
|
||||
format: DiagramFormat = DiagramFormat.DRAWIO
|
||||
): Readonly<DiagramData['metadata']> => ({
|
||||
type,
|
||||
format,
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
});
|
||||
|
||||
// Pure function to update diagram metadata
|
||||
export const updateDiagramMetadata = (
|
||||
metadata: DiagramData['metadata'],
|
||||
updates: Partial<DiagramData['metadata']>
|
||||
): Readonly<DiagramData['metadata']> => ({
|
||||
...metadata,
|
||||
...updates,
|
||||
modified: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Higher-order function to transform diagram data
|
||||
export const transformDiagramData = <T>(
|
||||
transformer: (data: DiagramData) => T
|
||||
) => (data: DiagramData): T => transformer(data);
|
||||
|
||||
// Pure function to merge diagram data
|
||||
export const mergeDiagramData = (
|
||||
base: DiagramData,
|
||||
additional: DiagramData
|
||||
): DiagramData => ({
|
||||
elements: [...base.elements, ...additional.elements],
|
||||
connections: [...base.connections, ...additional.connections],
|
||||
metadata: updateDiagramMetadata(base.metadata, {
|
||||
type: base.metadata.type // Keep base type
|
||||
})
|
||||
});
|
||||
|
||||
// Pure function to filter diagram elements by type
|
||||
export const filterElementsByType = (elementType: string) => (data: DiagramData): DiagramData => ({
|
||||
...data,
|
||||
elements: data.elements.filter(element => element.type === elementType),
|
||||
metadata: updateDiagramMetadata(data.metadata, {})
|
||||
});
|
||||
|
||||
// Pure function to map over diagram elements
|
||||
export const mapDiagramElements = <T>(
|
||||
mapper: (element: DiagramElement) => T
|
||||
) => (data: DiagramData): T[] => data.elements.map(mapper);
|
||||
|
||||
// Pure function to map over diagram connections
|
||||
export const mapDiagramConnections = <T>(
|
||||
mapper: (connection: DiagramConnection) => T
|
||||
) => (data: DiagramData): T[] => data.connections.map(mapper);
|
Reference in New Issue
Block a user