CI / build (push) Failing after 6m2s
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.
236 lines
6.6 KiB
JavaScript
236 lines
6.6 KiB
JavaScript
#!/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);
|
||
});
|