//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')); }