Files
bruno f753b27542
CI / build (push) Failing after 6m2s
Add asset pipeline and professionalize theme
Introduce a Node build system for minified CSS/JS with content hashes,
add PWA support, i18n helpers, accessibility improvements, and more
translatable strings. Move theme configuration from bookmarks to
localStorage, add Playwright tests, and set up CI workflows.
2026-08-09 09:25:03 -04:00

236 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});