182 lines
6.1 KiB
JavaScript
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'));
|
|
}
|