Add minify build script and generate .min CSS/JS; rename position to brand-aside

Build tooling:
- Add package.json with clean-css and terser dev dependencies
- Add scripts/minify.js: reads joomla.asset.json, auto-detects source/.min
  pairs, and minifies all template-owned CSS and JS files
- Add node_modules/ to .gitignore

Generated .min files (all 6 manifest pairs):
- css/template.min.css      (17.8% saved)
- css/editor.min.css        (49.4% saved)
- css/theme/light.standard.min.css  (13.1% saved)
- css/theme/dark.standard.min.css   (14.4% saved)
- js/template.min.js        (58.2% saved)
- js/gtm.min.js             (62.3% saved)

Rename: header-aside → brand-aside (position, CSS class, language keys)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 20:22:09 -05:00
parent f0de3bf342
commit f0e7ccac6d
14 changed files with 219 additions and 7 deletions

188
scripts/minify.js Normal file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* # FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
* PATH: ./scripts/minify.js
* VERSION: 03.09.01
* BRIEF: Generates .min.css and .min.js files from the Joomla asset manifest
*/
'use strict';
const fs = require('fs');
const path = require('path');
const CleanCSS = require('clean-css');
const { minify: terserMinify } = require('terser');
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT = path.resolve(__dirname, '..');
const SRC_MEDIA = path.join(ROOT, 'src', 'media');
const ASSET_JSON = path.join(ROOT, 'src', 'joomla.asset.json');
// URI prefix used in the manifest — maps to SRC_MEDIA on disk.
// e.g. "media/templates/site/mokocassiopeia/css/template.css"
const URI_PREFIX = 'media/templates/site/mokocassiopeia/';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Resolve a manifest URI to an absolute disk path under src/media/.
*
* @param {string} uri e.g. "media/templates/site/mokocassiopeia/css/foo.css"
* @returns {string|null}
*/
function uriToPath(uri) {
if (!uri.startsWith(URI_PREFIX)) return null;
return path.join(SRC_MEDIA, uri.slice(URI_PREFIX.length));
}
/**
* Return true if the filename looks like an already-minified file or belongs
* to a vendor bundle we don't own.
*/
function isVendorOrUserFile(filePath) {
const rel = filePath.replace(SRC_MEDIA + path.sep, '');
return rel.startsWith('vendor' + path.sep)
|| path.basename(filePath).startsWith('user.');
}
// ---------------------------------------------------------------------------
// Pair detection
// ---------------------------------------------------------------------------
/**
* Read the asset manifest and return an array of { src, dest, type } pairs
* where dest is a minified version of src that doesn't already exist or is
* older than src.
*
* Pairing logic: for every non-.min asset, check whether the manifest also
* contains a corresponding .min asset. If so, that's our pair.
*/
function detectPairs(assets) {
// Build a lookup of all URIs in the manifest.
const uriSet = new Set(assets.map(a => a.uri));
const pairs = [];
for (const asset of assets) {
const { uri, type } = asset;
if (type !== 'style' && type !== 'script') continue;
// Skip already-minified entries.
if (/\.min\.(css|js)$/.test(uri)) continue;
// Derive the expected .min URI.
const minUri = uri.replace(/\.(css|js)$/, '.min.$1');
if (!uriSet.has(minUri)) continue;
const srcPath = uriToPath(uri);
const destPath = uriToPath(minUri);
if (!srcPath || !destPath) continue;
if (isVendorOrUserFile(srcPath)) continue;
if (!fs.existsSync(srcPath)) {
console.warn(` [skip] source missing: ${srcPath}`);
continue;
}
pairs.push({ src: srcPath, dest: destPath, type });
}
return pairs;
}
// ---------------------------------------------------------------------------
// Minifiers
// ---------------------------------------------------------------------------
async function minifyCSS(srcPath, destPath) {
const source = fs.readFileSync(srcPath, 'utf8');
const result = new CleanCSS({ level: 2, returnPromise: true });
const output = await result.minify(source);
if (output.errors && output.errors.length) {
throw new Error(output.errors.join('\n'));
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, output.styles, 'utf8');
const srcSize = Buffer.byteLength(source, 'utf8');
const destSize = Buffer.byteLength(output.styles, 'utf8');
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
return { srcSize, destSize, saving };
}
async function minifyJS(srcPath, destPath) {
const source = fs.readFileSync(srcPath, 'utf8');
const result = await terserMinify(source, {
compress: { drop_console: false },
mangle: true,
format: { comments: false }
});
if (!result.code) throw new Error('terser returned no output');
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, result.code, 'utf8');
const srcSize = Buffer.byteLength(source, 'utf8');
const destSize = Buffer.byteLength(result.code, 'utf8');
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
return { srcSize, destSize, saving };
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
(async () => {
const manifest = JSON.parse(fs.readFileSync(ASSET_JSON, 'utf8'));
const pairs = detectPairs(manifest.assets);
if (pairs.length === 0) {
console.log('No pairs found — nothing to minify.');
return;
}
console.log(`\nMinifying ${pairs.length} file(s)...\n`);
let ok = 0, fail = 0;
for (const { src, dest, type } of pairs) {
const label = path.relative(ROOT, src);
process.stdout.write(` ${label} ... `);
try {
const stats = type === 'style'
? await minifyCSS(src, dest)
: await minifyJS(src, dest);
const kb = n => (n / 1024).toFixed(1) + ' kB';
console.log(`${kb(stats.srcSize)}${kb(stats.destSize)} (${stats.saving}% saved)`);
ok++;
} catch (err) {
console.error(`FAILED\n ${err.message}`);
fail++;
}
}
console.log(`\nDone. ${ok} succeeded, ${fail} failed.\n`);
if (fail > 0) process.exit(1);
})();