UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

447 lines (398 loc) 12.9 kB
/** * Platform Utilities * * Utilities for cross-platform compatibility to ensure consistent behavior * across different operating systems (Windows, macOS, Linux). */ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; /** * Line ending types for file generation */ export type LineEnding = 'auto' | 'lf' | 'crlf'; /** * Line ending characters by type */ export const LINE_ENDINGS = { lf: '\n', crlf: '\r\n', auto: os.platform() === 'win32' ? '\r\n' : '\n' }; /** * Cross-platform file path separator functions */ export class PathUtils { /** * Normalize path separators for the current platform * @param filePath File path to normalize * @returns Normalized path */ static normalizePath(filePath: string): string { return filePath.replace(/[/\\]+/g, path.sep); } /** * Ensure a path uses forward slashes (useful for URL or config paths) * @param filePath File path to normalize * @returns Path with forward slashes */ static forwardSlashes(filePath: string): string { return filePath.replace(/\\/g, '/'); } /** * Convert a path to an absolute path, handling both relative and absolute inputs * @param filePath File path to resolve * @param basePath Base path for resolving relative paths (defaults to current directory) * @returns Absolute path */ static toAbsolutePath(filePath: string, basePath = process.cwd()): string { if (path.isAbsolute(filePath)) { return this.normalizePath(filePath); } return this.normalizePath(path.resolve(basePath, filePath)); } /** * Get a relative path from one location to another, handling cross-platform differences * @param from Source path * @param to Destination path * @returns Relative path */ static getRelativePath(from: string, to: string): string { const relativePath = path.relative(from, to); return this.normalizePath(relativePath); } /** * Ensure a directory exists, creating it if necessary * @param dirPath Directory path to ensure * @returns True if directory exists or was created */ static ensureDir(dirPath: string): boolean { try { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error creating directory ${dirPath}: ${errorMessage}`); return false; } } /** * Get a platform-appropriate temporary directory path * @param subdir Optional subdirectory name * @returns Path to temporary directory */ static getTempDir(subdir?: string): string { const tempBase = os.tmpdir(); const dirPath = subdir ? path.join(tempBase, subdir) : tempBase; this.ensureDir(dirPath); return dirPath; } /** * Determine if a path exists and is a directory * @param dirPath Path to check * @returns True if path is a directory */ static isDirectory(dirPath: string): boolean { try { return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory(); } catch (error) { return false; } } /** * Determine if a path exists and is a file * @param filePath Path to check * @returns True if path is a file */ static isFile(filePath: string): boolean { try { return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); } catch (error) { return false; } } } /** * Cross-platform file content utilities */ export class FileUtils { /** * Ensure file content has consistent line endings * @param content File content to normalize * @param lineEnding Line ending type to use * @returns Normalized content */ static normalizeLineEndings(content: string, lineEnding: LineEnding = 'auto'): string { // First convert all line endings to LF const normalized = content.replace(/\r\n|\r/g, '\n'); // Then convert to the desired line ending if (lineEnding === 'crlf') { return normalized.replace(/\n/g, '\r\n'); } if (lineEnding === 'auto' && os.platform() === 'win32') { return normalized.replace(/\n/g, '\r\n'); } return normalized; } /** * Write file with consistent line endings * @param filePath Path to write to * @param content Content to write * @param lineEnding Line ending type to use * @returns True if successful */ static writeFile(filePath: string, content: string, lineEnding: LineEnding = 'auto'): boolean { try { const normalized = this.normalizeLineEndings(content, lineEnding); const dir = path.dirname(filePath); // Ensure directory exists if (!PathUtils.ensureDir(dir)) { return false; } fs.writeFileSync(filePath, normalized); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error writing file ${filePath}: ${errorMessage}`); return false; } } /** * Read file and normalize line endings * @param filePath Path to read from * @param lineEnding Line ending type to use * @returns File content or null if error */ static readFile(filePath: string, lineEnding: LineEnding = 'auto'): string | null { try { if (!PathUtils.isFile(filePath)) { return null; } const content = fs.readFileSync(filePath, 'utf8'); return this.normalizeLineEndings(content, lineEnding); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error reading file ${filePath}: ${errorMessage}`); return null; } } /** * Append to file with consistent line endings * @param filePath Path to append to * @param content Content to append * @param lineEnding Line ending type to use * @returns True if successful */ static appendFile(filePath: string, content: string, lineEnding: LineEnding = 'auto'): boolean { try { let normalized = this.normalizeLineEndings(content, lineEnding); // Add a line break at the beginning if file exists and doesn't end with one if (PathUtils.isFile(filePath)) { const existing = fs.readFileSync(filePath, 'utf8'); const ending = lineEnding === 'crlf' ? '\r\n' : (lineEnding === 'auto' && os.platform() === 'win32') ? '\r\n' : '\n'; if (!existing.endsWith(ending)) { normalized = ending + normalized; } } const dir = path.dirname(filePath); // Ensure directory exists if (!PathUtils.ensureDir(dir)) { return false; } fs.appendFileSync(filePath, normalized); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error appending to file ${filePath}: ${errorMessage}`); return false; } } } /** * Cross-platform test output generator */ export class TestOutputUtils { /** * Generate a platform-appropriate path for test outputs * @param componentPath Path to component being tested * @param outputBase Base directory for output * @param fileExtension File extension for output file * @returns Path to test output file */ static getTestOutputPath( componentPath: string, outputBase: string = 'tests', fileExtension: string = '.spec.ts' ): string { // Extract component name from path const componentName = path.basename(componentPath, path.extname(componentPath)); // Handle different naming conventions: // foo-bar.jsx -> FooBar.spec.ts or foo-bar.spec.ts const camelCaseName = componentName .split(/[-_]/) .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); // Ensure output directory exists const outputDir = path.join(process.cwd(), outputBase); PathUtils.ensureDir(outputDir); // Create component subdirectory for better organization const componentDir = path.join(outputDir, camelCaseName); PathUtils.ensureDir(componentDir); // Return path with appropriate extension return path.join(componentDir, `${camelCaseName}${fileExtension}`); } /** * Generate configuration file with platform-appropriate paths * @param configType Type of configuration (playwright, jest) * @param outputDir Output directory * @param lineEnding Line ending type to use * @returns Generated configuration content */ static generateTestConfig( configType: 'playwright' | 'jest', outputDir: string, lineEnding: LineEnding = 'auto' ): string { // Normalize path for the config file const normalizedOutputDir = PathUtils.forwardSlashes(outputDir); let content = ''; if (configType === 'playwright') { content = `import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './', outputDir: '../test-results', timeout: 30000, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [['html', { outputFolder: '../test-report' }]], use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], });`; } else { content = `module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], moduleNameMapper: { '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy', '^@/(.*)$': '<rootDir>/src/$1', }, setupFilesAfterEnv: ['./jest.setup.js'], collectCoverage: true, collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/mocks/**', ], coverageDirectory: '../coverage', };`; } return FileUtils.normalizeLineEndings(content, lineEnding); } } /** * Cross-platform shell command utilities */ export class ShellUtils { /** * Get platform-appropriate shell command * @param command Base command * @param args Command arguments * @returns Platform-specific command string */ static getCommand(command: string, args: string[] = []): string { const isWindows = os.platform() === 'win32'; // Handle commands that need special treatment on Windows if (isWindows) { // Common commands that need to be mapped on Windows const windowsCommandMap: Record<string, string> = { 'rm': 'del', 'cp': 'copy', 'mv': 'move', 'ls': 'dir', 'cat': 'type', 'mkdir': 'md', 'rmdir': 'rd', 'touch': 'echo.>', 'grep': 'findstr' }; // Map command if needed const mappedCommand = windowsCommandMap[command] || command; // Build the command string return [mappedCommand, ...args].join(' '); } // For non-Windows platforms, just join the command and args return [command, ...args].join(' '); } /** * Get platform-appropriate file path for a command * @param filePath File path to use in command * @returns Formatted file path string */ static getCommandPath(filePath: string): string { const isWindows = os.platform() === 'win32'; // On Windows, wrap paths with spaces in quotes if (isWindows && filePath.includes(' ')) { return `"${filePath}"`; } // On Unix-like systems, escape spaces return filePath.replace(/ /g, '\\ '); } /** * Get platform-appropriate npm script command * @param scriptName NPM script name * @param args Script arguments * @returns Platform-specific npm command string */ static getNpmCommand(scriptName: string, args: string[] = []): string { const isWindows = os.platform() === 'win32'; const npmCmd = isWindows ? 'npm.cmd' : 'npm'; // Build the command string return [npmCmd, 'run', scriptName, '--', ...args].join(' '); } } /** * Detect the operating system and provide platform information */ export function getPlatformInfo(): { platform: string; isWindows: boolean; isMac: boolean; isLinux: boolean; arch: string; pathSeparator: string; lineEnding: string; } { const platform = os.platform(); return { platform, isWindows: platform === 'win32', isMac: platform === 'darwin', isLinux: platform === 'linux', arch: os.arch(), pathSeparator: path.sep, lineEnding: platform === 'win32' ? '\r\n' : '\n' }; }