Extract YAML validation to separate module
- Created yaml-validator.js with pure validation functions - Removed redundant checkYAMLFormatting function (rely on js-yaml) - Fixed function shadowing issues by using window._validateYAMLPure - Updated nginx config to serve JS files with correct MIME type - Improved error reporting with line/column numbers from js-yaml
This commit is contained in:
parent
4e108c7d7f
commit
8d2def360e
4 changed files with 201 additions and 116 deletions
137
yaml-validator.js
Normal file
137
yaml-validator.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue