#!/usr/bin/env node /** * Build pipeline for the Shaarli Pro theme. * * Actions: * - Minify CSS with PostCSS + cssnano. * - Minify JavaScript with Terser. * - Compute content hashes for cache-busting. * - Emit a manifest.json in dist/. * * Usage: * npm run build # full build * npm run build:css # CSS only * npm run build:js # JS only */ 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const terserAvailable = (function () { try { require.resolve('terser'); return true; } catch (e) { return false; } })(); const postcssAvailable = (function () { try { require.resolve('postcss'); require.resolve('cssnano'); return true; } catch (e) { return false; } })(); const ROOT = __dirname; const DIST_DIR = path.join(ROOT, 'dist'); const MANIFEST_PATH = path.join(DIST_DIR, 'manifest.json'); const cssFiles = [ 'css/style.css', 'css/themes.css', 'css/custom_views.css', 'css/awesomplete.css' ]; const jsFiles = [ 'js/i18n.js', 'js/shaarit-rules.js', 'js/modules/a11y.js', 'js/modules/theme.js', 'js/modules/navigation.js', 'js/modules/search.js', 'js/modules/bulk-actions.js', 'js/script.js', 'js/custom_views.js', // Already minified vendor files – copy them as-is but still hash them. 'js/awesomplete.min.js', 'js/metadata.min.js' ]; const args = process.argv.slice(2); const cssOnly = args.includes('--css-only'); const jsOnly = args.includes('--js-only'); const buildCss = !jsOnly; const buildJs = !cssOnly; function hash(content) { return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12); } function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } async function minifyCss(file) { if (!postcssAvailable) { throw new Error('PostCSS / cssnano dependencies are missing. Run npm install.'); } const postcss = require('postcss'); const cssnano = require('cssnano'); const inputPath = path.join(ROOT, file); const content = fs.readFileSync(inputPath, 'utf8'); const result = await postcss([cssnano({ preset: 'default' })]).process(content, { from: inputPath, to: inputPath }); return result.css; } async function minifyJs(file) { if (!terserAvailable) { throw new Error('Terser dependency is missing. Run npm install.'); } const { minify } = require('terser'); const inputPath = path.join(ROOT, file); const content = fs.readFileSync(inputPath, 'utf8'); const isVendor = file.includes('.min.js'); if (isVendor) { // Vendor files are already minified; just validate syntax and copy. return content; } const result = await minify(content, { compress: { drop_console: false, // keep user-facing console warnings drop_debugger: true, passes: 1 }, mangle: { reserved: ['shaarli', 'ShaarliProI18n', 'ShaarliProUtils', 'ShaarliProA11y'] }, format: { comments: /^!|@preserve|@license|copyright/i }, sourceMap: false }); if (result.error) { throw new Error(`Terser error in ${file}: ${result.error}`); } return result.code; } async function build() { const start = Date.now(); if (fs.existsSync(DIST_DIR)) { fs.rmSync(DIST_DIR, { recursive: true, force: true }); } ensureDir(DIST_DIR); const manifest = { generatedAt: new Date().toISOString(), files: {} }; const processed = []; const errors = []; if (buildCss) { for (const file of cssFiles) { const inputPath = path.join(ROOT, file); if (!fs.existsSync(inputPath)) { errors.push(`CSS file not found: ${file}`); continue; } try { const minified = await minifyCss(file); const fileHash = hash(minified); const baseName = path.basename(file, '.css'); const ext = path.extname(file); const outputName = `${baseName}.min${ext}`; const outputPath = path.join(DIST_DIR, outputName); fs.writeFileSync(outputPath, minified, 'utf8'); manifest.files[file] = { dist: `dist/${outputName}`, hash: fileHash, size: minified.length }; processed.push(`css/${outputName}`); } catch (e) { errors.push(`CSS ${file}: ${e.message}`); } } } if (buildJs) { for (const file of jsFiles) { const inputPath = path.join(ROOT, file); if (!fs.existsSync(inputPath)) { errors.push(`JS file not found: ${file}`); continue; } try { const minified = await minifyJs(file); const fileHash = hash(minified); const baseName = path.basename(file, '.js'); const ext = path.extname(file); // Avoid double .min for vendor files already minified. const outputName = baseName.endsWith('.min') ? `${baseName}${ext}` : `${baseName}.min${ext}`; const outputPath = path.join(DIST_DIR, outputName); fs.writeFileSync(outputPath, minified, 'utf8'); manifest.files[file] = { dist: `dist/${outputName}`, hash: fileHash, size: minified.length }; processed.push(`js/${outputName}`); } catch (e) { errors.push(`JS ${file}: ${e.message}`); } } } fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf8'); const duration = Date.now() - start; if (errors.length > 0) { console.error('\nBuild completed with errors:'); for (const err of errors) { console.error(` ✗ ${err}`); } } if (processed.length > 0) { console.log(`\nBuilt ${processed.length} file(s) in ${duration}ms:`); for (const name of processed) { console.log(` ✓ ${name}`); } } console.log(`Manifest written to ${path.relative(ROOT, MANIFEST_PATH)}`); if (errors.length > 0) { process.exit(1); } } build().catch(err => { console.error('Build failed:', err); process.exit(1); });