Files
builderman/index.js

182 lines
6.1 KiB
JavaScript

//builderman
const fs = require('fs-extra');
const path = require('path');
const CleanCSS = require('clean-css');
const minify = require('minify');
const { XMLParser } = require('fast-xml-parser');
const cheerio = require('cheerio');
const cssMinifier = new CleanCSS({
level: 2,
compatibility: '*'
});
// Error handler function
// ts = error message, err = error type ('nofile' or 'generic')
// In order to implement error logging, we need to wait untill we load the config.
// Otherwise I can guarantee that shenanigans will occur
function fixts(ts, err) {
if (err === 'nofile') {
console.error(`There be nothing: ${ts}`);
} else {
//NOTE: Sometimes "generic" will be called. we don't actually check it. if we get an error that isnt nofile, it's generic.
console.error(`This code sucks: ${ts}`);
}
}
async function loadConfig(configPath) {
if (!await fs.pathExists(configPath)) {
fixts(configPath, 'nofile');
throw new Error('Config file not found');
}
const configContent = await fs.readFile(configPath, 'utf8');
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
allowBooleanAttributes: true,
parseTagValue: true,
isArray: (name, jpath, isLeafNode, isAttribute) => {
return name === 'build' || name === 'tag';
}
});
try {
const config = parser.parse(configContent);
return config.builds.build;
} catch (err) {
fixts('Failed to parse config.xml', 'generic');
throw err;
}
}
async function processHTML(htmlContent, buildConfig) {
const $ = cheerio.load(htmlContent, { xmlMode: false });
// Handle includes - if specified, keep only those tags
if (buildConfig.include && buildConfig.include.tag) {
const includes = Array.isArray(buildConfig.include.tag)
? buildConfig.include.tag
: [buildConfig.include.tag];
// Remove non-included tags at root level only when includes are specified
$('body > *').each((_, elem) => {
if (!includes.includes(elem.tagName)) {
$(elem).remove();
}
});
}
// No includes specified means keep everything (except excludes)
// Handle excludes - always remove specified tags
if (buildConfig.exclude && buildConfig.exclude.tag) {
const excludes = Array.isArray(buildConfig.exclude.tag)
? buildConfig.exclude.tag
: [buildConfig.exclude.tag];
excludes.forEach(tag => {
$(tag).remove();
});
}
return $.html();
}
async function processBuild(inputDir, outputDir, buildConfig) {
if (!await fs.pathExists(inputDir)) {
fixts(inputDir, 'nofile');
throw new Error('Input directory not found');
}
const buildOutputDir = path.join(outputDir, buildConfig.name);
await fs.ensureDir(buildOutputDir);
// Process HTML template
const htmlFiles = await fs.readdir(inputDir);
for (const file of htmlFiles) {
if (file.endsWith('.html')) {
const inputFile = path.join(inputDir, file);
if (!await fs.pathExists(inputFile)) {
fixts(inputFile, 'nofile');
continue;
}
try {
const htmlContent = await fs.readFile(inputFile, 'utf8');
const processedHTML = await processHTML(htmlContent, buildConfig);
// Save the processed HTML to a temporary file for minification
const tempFile = path.join(buildOutputDir, 'temp_' + file);
await fs.writeFile(tempFile, processedHTML, 'utf8');
// Minify using the file path
const minified = await minify(tempFile);
await fs.writeFile(
path.join(buildOutputDir, file),
minified,
'utf8'
);
// Clean up temp file
await fs.remove(tempFile);
} catch (err) {
fixts(`Error processing ${file}: ${err}`, 'generic');
// On error, at least save the unminified version
const debugFile = path.join(buildOutputDir, 'debug_' + file);
await fs.writeFile(debugFile, processedHTML, 'utf8');
}
}
}
// Process associated stylesheet
if (buildConfig.style && buildConfig.style.src) {
const cssFile = path.join(inputDir, buildConfig.style.src);
if (!await fs.pathExists(cssFile)) {
fixts(cssFile, 'nofile');
return;
}
try {
const cssContent = await fs.readFile(cssFile, 'utf8');
const minified = cssMinifier.minify(cssContent);
await fs.writeFile(
path.join(buildOutputDir, buildConfig.style.src),
minified.styles,
'utf8'
);
} catch (err) {
fixts(`Error processing CSS file ${buildConfig.style.src}: ${err}`, 'generic');
}
}
}
async function processFiles(inputDir, outputDir) {
if (!await fs.pathExists(inputDir)) {
fixts(inputDir, 'nofile');
throw new Error('Input directory not found');
}
// Remove old dist directory if it exists
await fs.remove(outputDir);
await fs.ensureDir(outputDir);
try {
// Load and parse config
const configPath = path.join(inputDir, 'config.xml');
const builds = await loadConfig(configPath);
// Process each build configuration
const buildConfigs = Array.isArray(builds) ? builds : [builds];
for (const buildConfig of buildConfigs) {
await processBuild(inputDir, outputDir, buildConfig);
}
} catch (err) {
fixts(`Build process failed: ${err}`, 'generic');
throw err;
}
}
// Run if called directly
if (require.main === module) {
const inputDir = process.argv[2] || 'src';
const outputDir = process.argv[3] || 'dist';
processFiles(inputDir, outputDir).catch(err => fixts(err, 'generic'));
}