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:
robojerk 2025-10-31 13:28:38 -07:00
parent 4e108c7d7f
commit 8d2def360e
4 changed files with 201 additions and 116 deletions

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stronghold - Minecraft Server Generator</title>
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
<script src="yaml-validator.js"></script>
<style>
* {
margin: 0;
@ -697,138 +698,78 @@
const yamlStatus = document.getElementById('yamlStatus');
let currentYaml = '';
// 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;
}
// YAML validation function
// YAML validation function (wrapper that handles DOM updates)
function validateYAML(yamlText) {
yamlError.classList.remove('show');
yamlSuccess.classList.remove('show');
const errorElement = document.getElementById('yamlError');
const successElement = document.getElementById('yamlSuccess');
errorElement.classList.remove('show');
successElement.classList.remove('show');
yamlEditor.classList.remove('error', 'valid');
yamlStatus.textContent = '';
yamlStatus.classList.remove('valid', 'invalid');
if (!yamlText || yamlText.trim() === '') {
yamlStatus.textContent = 'YAML is empty';
// Get the pure validation function from yaml-validator.js
// yaml-validator.js exposes it as window._validateYAMLPure to avoid shadowing
const pureValidateFn = (typeof window !== 'undefined' && window._validateYAMLPure)
? window._validateYAMLPure
: null;
if (!pureValidateFn) {
errorElement.textContent = 'Error: YAML validator library not loaded. Please refresh the page.';
errorElement.classList.add('show');
yamlStatus.textContent = '✗ Validation Error';
yamlStatus.classList.add('invalid');
return { valid: false, error: 'YAML is empty' };
console.error('Cannot access _validateYAMLPure from yaml-validator.js');
return { valid: false, error: 'YAML validator not loaded', config: null };
}
// Call the pure function from yaml-validator.js
const result = pureValidateFn(yamlText);
let parsedConfig;
let yamlError = null;
// First, check YAML syntax
try {
parsedConfig = jsyaml.load(yamlText);
} catch (error) {
if (!result.valid) {
// Show error
errorElement.textContent = result.error;
errorElement.classList.add('show');
successElement.classList.remove('show');
yamlEditor.classList.add('error');
yamlEditor.classList.remove('valid');
yamlError = error;
}
if (yamlError) {
// YAML syntax error
document.getElementById('yamlError').textContent = `YAML Syntax Error: ${yamlError.message}`;
document.getElementById('yamlError').classList.add('show');
yamlSuccess.classList.remove('show');
yamlStatus.textContent = '✗ Invalid YAML Syntax';
// Determine status text based on error type
if (result.error.includes('Syntax Error')) {
yamlStatus.textContent = '✗ Invalid YAML Syntax';
} else if (result.error.includes('Docker Compose')) {
yamlStatus.textContent = '✗ Docker Compose Validation Failed';
} else {
yamlStatus.textContent = '✗ Validation Error';
}
yamlStatus.classList.add('invalid');
return { valid: false, error: yamlError.message };
} else {
// All valid!
yamlEditor.classList.add('valid');
yamlEditor.classList.remove('error');
errorElement.classList.remove('show');
successElement.classList.add('show');
yamlStatus.textContent = '✓ Valid Docker Compose YAML';
yamlStatus.classList.add('valid');
}
// YAML is syntactically valid, now check Docker Compose structure
const composeErrors = validateDockerCompose(parsedConfig);
return result;
}
if (composeErrors.length > 0) {
// Docker Compose structure errors
yamlEditor.classList.add('error');
yamlEditor.classList.remove('valid');
document.getElementById('yamlError').textContent = `Docker Compose Errors:\n• ${composeErrors.join('\n• ')}`;
document.getElementById('yamlError').classList.add('show');
yamlSuccess.classList.remove('show');
yamlStatus.textContent = '✗ Docker Compose Validation Failed';
yamlStatus.classList.add('invalid');
return { valid: false, error: composeErrors.join('; ') };
}
// All valid!
yamlEditor.classList.add('valid');
yamlEditor.classList.remove('error');
document.getElementById('yamlError').classList.remove('show');
yamlSuccess.classList.add('show');
yamlStatus.textContent = '✓ Valid Docker Compose YAML';
yamlStatus.classList.add('valid');
return { valid: true };
// Check if js-yaml is loaded
if (typeof jsyaml === 'undefined') {
console.error('js-yaml library not loaded!');
yamlStatus.textContent = 'Error: YAML library not loaded';
yamlStatus.classList.add('invalid');
}
// Real-time YAML linting with debounce
let lintTimeout;
yamlEditor.addEventListener('input', () => {
if (typeof jsyaml === 'undefined') {
return; // Don't validate if library isn't loaded
}
clearTimeout(lintTimeout);
lintTimeout = setTimeout(() => {
validateYAML(yamlEditor.value);