blob: 0c58bddb974bd1cf990cc3542148f97935bbab8b [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Build Validation Script
*
* Validates the production build output:
* - Build directory exists
* - Expected files are present
* - HTML files are valid
* - Search index is generated
* - Static assets are present
*/
const fs = require('fs');
const path = require('path');
const buildDir = path.join(__dirname, '../build');
// ANSI color codes
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
gray: '\x1b[90m'
};
// Results tracking
const results = {
checks: [],
warnings: [],
errors: []
};
/**
* Add a check result
*/
function check(name, passed, message = '') {
results.checks.push({ name, passed, message });
if (!passed) {
results.errors.push({ name, message });
}
}
/**
* Add a warning
*/
function warn(name, message) {
results.warnings.push({ name, message });
}
/**
* Check if build directory exists
*/
function checkBuildDir() {
const exists = fs.existsSync(buildDir) && fs.statSync(buildDir).isDirectory();
check('Build directory exists', exists, exists ? '' : 'Run npm run build first');
return exists;
}
/**
* Check if index.html exists
*/
function checkIndexHtml() {
const indexPath = path.join(buildDir, 'index.html');
const exists = fs.existsSync(indexPath);
check('index.html exists', exists);
return exists;
}
/**
* Count HTML files in build
*/
function countHtmlFiles(dir) {
let count = 0;
function traverse(currentDir) {
if (!fs.existsSync(currentDir)) return;
const files = fs.readdirSync(currentDir);
files.forEach(file => {
const filePath = path.join(currentDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
traverse(filePath);
} else if (file.endsWith('.html')) {
count++;
}
});
}
traverse(dir);
return count;
}
/**
* Check HTML file generation
*/
function checkHtmlFiles() {
const count = countHtmlFiles(buildDir);
const passed = count > 0;
check('HTML files generated', passed, passed ? `Found ${count} HTML files` : 'No HTML files found');
return passed;
}
/**
* Check search index generation
*/
function checkSearchIndex() {
const searchIndexPath = path.join(buildDir, 'search-index.json');
const exists = fs.existsSync(searchIndexPath);
if (exists) {
try {
const content = fs.readFileSync(searchIndexPath, 'utf8');
const data = JSON.parse(content);
const hasEntries = Array.isArray(data) && data.length > 0;
check('Search index generated', hasEntries, hasEntries ? `${data.length} entries` : 'Index is empty');
return hasEntries;
} catch (e) {
check('Search index valid', false, `Invalid JSON: ${e.message}`);
return false;
}
} else {
check('Search index generated', false, 'search-index.json not found');
return false;
}
}
/**
* Check assets directory
*/
function checkAssets() {
const assetsDir = path.join(buildDir, 'assets');
const exists = fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory();
if (exists) {
const files = fs.readdirSync(assetsDir);
const jsFiles = files.filter(f => f.endsWith('.js'));
const cssFiles = files.filter(f => f.endsWith('.css'));
check('Assets directory exists', true, `${jsFiles.length} JS, ${cssFiles.length} CSS files`);
if (jsFiles.length === 0) {
warn('No JavaScript files', 'Expected JS bundles in assets/');
}
if (cssFiles.length === 0) {
warn('No CSS files', 'Expected CSS files in assets/');
}
return true;
} else {
check('Assets directory exists', false);
return false;
}
}
/**
* Check static files
*/
function checkStaticFiles() {
const imgDir = path.join(buildDir, 'img');
const exists = fs.existsSync(imgDir);
if (exists) {
const files = fs.readdirSync(imgDir);
check('Static files copied', true, `${files.length} files in img/`);
} else {
warn('Static files', 'img/ directory not found');
}
// Check for favicon
const faviconPath = path.join(buildDir, 'img', 'favicon.ico');
if (fs.existsSync(faviconPath)) {
check('Favicon present', true);
} else {
warn('Favicon', 'favicon.ico not found in img/');
}
}
/**
* Check version directories
*/
function checkVersions() {
const version310Dir = path.join(buildDir, '3.1.0');
const version300Dir = path.join(buildDir, '3.0.0');
const v310Exists = fs.existsSync(version310Dir) && fs.statSync(version310Dir).isDirectory();
const v300Exists = fs.existsSync(version300Dir) && fs.statSync(version300Dir).isDirectory();
check('Version 3.1.0 directory', v310Exists);
check('Version 3.0.0 directory', v300Exists);
if (v310Exists) {
const indexPath = path.join(version310Dir, 'index.html');
check('Version 3.1.0 index.html', fs.existsSync(indexPath));
}
if (v300Exists) {
const indexPath = path.join(version300Dir, 'index.html');
check('Version 3.0.0 index.html', fs.existsSync(indexPath));
}
}
/**
* Calculate build size
*/
function calculateBuildSize(dir) {
let totalSize = 0;
function traverse(currentDir) {
if (!fs.existsSync(currentDir)) return;
const files = fs.readdirSync(currentDir);
files.forEach(file => {
const filePath = path.join(currentDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
traverse(filePath);
} else {
totalSize += stat.size;
}
});
}
traverse(dir);
return totalSize;
}
/**
* Format bytes to human-readable size
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
/**
* Print results summary
*/
function printResults() {
console.log('\n' + colors.blue + '=== Build Validation Results ===' + colors.reset);
// Print checks
console.log('\n' + colors.gray + '--- Checks ---' + colors.reset);
results.checks.forEach(c => {
const icon = c.passed ? colors.green + '✓' : colors.red + '✗';
const status = c.passed ? colors.green : colors.red;
console.log(`${icon} ${status}${c.name}${colors.reset}${c.message ? colors.gray + ' (' + c.message + ')' + colors.reset : ''}`);
});
// Print warnings
if (results.warnings.length > 0) {
console.log('\n' + colors.yellow + `--- Warnings (${results.warnings.length}) ---` + colors.reset);
results.warnings.forEach(w => {
console.log(colors.yellow + ` ${w.name}` + colors.reset);
console.log(colors.gray + ` ${w.message}` + colors.reset);
});
}
// Build size
const buildSize = calculateBuildSize(buildDir);
console.log('\n' + colors.gray + `Build size: ${formatBytes(buildSize)}` + colors.reset);
// Final result
if (results.errors.length > 0) {
console.log('\n' + colors.red + `!!! Build validation FAILED (${results.errors.length} errors)` + colors.reset);
process.exit(1);
} else if (results.warnings.length > 0) {
console.log('\n' + colors.yellow + '<<< Build validation PASSED with warnings' + colors.reset);
} else {
console.log('\n' + colors.green + '<<< Build validation PASSED' + colors.reset);
}
}
// Main execution
console.log(colors.blue + '>>> Validating production build' + colors.reset);
if (checkBuildDir()) {
checkIndexHtml();
checkHtmlFiles();
checkSearchIndex();
checkAssets();
checkStaticFiles();
checkVersions();
}
printResults();