/** * YAML Linting and Docker Compose Validation * * Pure validation functions for YAML syntax, formatting, and Docker Compose structure. * Requires js-yaml library to be loaded globally. * * Note: We rely on js-yaml for ALL syntax and formatting validation, including indentation. * The js-yaml parser is comprehensive and handles all YAML edge cases correctly. */ // Docker Compose structure validation function validateDockerCompose(config) { const errors = []; // Check for required top-level keys if (!config.services) { errors.push('Missing "services" section'); return errors; } // Check services const serviceNames = Object.keys(config.services); if (serviceNames.length === 0) { errors.push('No services defined'); } // Validate each service serviceNames.forEach(serviceName => { const service = config.services[serviceName]; // Check for required service fields if (!service.image && !service.build) { errors.push(`Service "${serviceName}": Missing "image" or "build"`); } // Check volume references match defined volumes if (service.volumes && Array.isArray(service.volumes)) { service.volumes.forEach(volume => { // Extract volume name from volume string (format: "volume_name:/path" or "/host:/path") if (typeof volume === 'string') { const parts = volume.split(':'); const volumeName = parts[0]; // Check if it's a named volume (not a bind mount starting with / or ./) if (volumeName && !volumeName.startsWith('/') && !volumeName.startsWith('./') && volumeName !== '~') { // Check if volume is defined if (config.volumes && !config.volumes.hasOwnProperty(volumeName)) { errors.push(`Service "${serviceName}": Volume "${volumeName}" is referenced but not defined in "volumes" section`); } } } }); } // Validate port format if (service.ports && Array.isArray(service.ports)) { service.ports.forEach(port => { if (typeof port === 'string') { // Basic port format check: "host:container" or just number const parts = port.split(':'); if (parts.length === 2 && (!parts[0].match(/^\d+$/) || !parts[1].match(/^\d+\/?\w*$/))) { errors.push(`Service "${serviceName}": Invalid port format "${port}" (expected "host:container")`); } } }); } // Check environment variables format if (service.environment) { if (typeof service.environment !== 'object') { errors.push(`Service "${serviceName}": "environment" must be an object or array`); } } }); return errors; } /** * Main YAML validation function (pure function, no DOM manipulation) * @param {string} yamlText - The YAML text to validate * @returns {Object} Validation result with {valid: boolean, error: string|null, config: Object|null} */ function validateYAMLPure(yamlText) { // Check if js-yaml is available if (typeof jsyaml === 'undefined') { return { valid: false, error: 'YAML validation library not loaded', config: null }; } if (!yamlText || yamlText.trim() === '') { return { valid: true, error: null, config: null }; // Treat empty as valid (no errors) } let parsedConfig; // 1. Rely on js-yaml for ALL syntax/formatting checks (Indentation, spacing, etc.) try { parsedConfig = jsyaml.load(yamlText); } catch (error) { // Extract precise location data from the js-yaml error object const line = error.mark ? error.mark.line + 1 : 'Unknown'; const column = error.mark ? error.mark.column + 1 : 'Unknown'; return { valid: false, error: `YAML Syntax Error (Line ${line}, Col ${column}): ${error.reason}`, config: null }; } // A valid YAML parse might return null or undefined for an empty file if (!parsedConfig) { return { valid: true, error: null, config: null }; } // 2. YAML is syntactically valid, now check Docker Compose structure const composeErrors = validateDockerCompose(parsedConfig); if (composeErrors.length > 0) { return { valid: false, error: `Docker Compose Structural Errors:\n• ${composeErrors.join('\n• ')}`, config: parsedConfig }; } // All valid! return { valid: true, error: 'YAML is valid and meets basic Docker Compose requirements.', config: parsedConfig }; } // Expose the function on window with a unique name to avoid conflicts // This must happen at the end of the script, after the function is defined if (typeof window !== 'undefined') { window._validateYAMLPure = validateYAMLPure; console.log('Exposed _validateYAMLPure on window'); }