ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
428 lines (427 loc) • 14.9 kB
JavaScript
;
/**
* Platform Utilities
*
* Utilities for cross-platform compatibility to ensure consistent behavior
* across different operating systems (Windows, macOS, Linux).
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShellUtils = exports.TestOutputUtils = exports.FileUtils = exports.PathUtils = exports.LINE_ENDINGS = void 0;
exports.getPlatformInfo = getPlatformInfo;
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const fs = __importStar(require("fs"));
/**
* Line ending characters by type
*/
exports.LINE_ENDINGS = {
lf: '\n',
crlf: '\r\n',
auto: os.platform() === 'win32' ? '\r\n' : '\n'
};
/**
* Cross-platform file path separator functions
*/
class PathUtils {
/**
* Normalize path separators for the current platform
* @param filePath File path to normalize
* @returns Normalized path
*/
static normalizePath(filePath) {
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) {
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, basePath = process.cwd()) {
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, to) {
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) {
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) {
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) {
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) {
try {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
}
catch (error) {
return false;
}
}
}
exports.PathUtils = PathUtils;
/**
* Cross-platform file content utilities
*/
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, lineEnding = 'auto') {
// 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, content, lineEnding = 'auto') {
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, lineEnding = 'auto') {
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, content, lineEnding = 'auto') {
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;
}
}
}
exports.FileUtils = FileUtils;
/**
* Cross-platform test output generator
*/
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, outputBase = 'tests', fileExtension = '.spec.ts') {
// 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, outputDir, lineEnding = 'auto') {
// 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);
}
}
exports.TestOutputUtils = TestOutputUtils;
/**
* Cross-platform shell command utilities
*/
class ShellUtils {
/**
* Get platform-appropriate shell command
* @param command Base command
* @param args Command arguments
* @returns Platform-specific command string
*/
static getCommand(command, args = []) {
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 = {
'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) {
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, args = []) {
const isWindows = os.platform() === 'win32';
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
// Build the command string
return [npmCmd, 'run', scriptName, '--', ...args].join(' ');
}
}
exports.ShellUtils = ShellUtils;
/**
* Detect the operating system and provide platform information
*/
function getPlatformInfo() {
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'
};
}
//# sourceMappingURL=platformUtils.js.map