fix(docker): send ROBOTS_NOINDEX as a header, make COMPRESSION_MODE work and fix the Docker guide example

This commit is contained in:
alam00000
2026-10-27 18:07:29 +05:30
parent 1a6b38c077
commit d8210be3c1
37 changed files with 244 additions and 408 deletions
+59 -205
View File
@@ -1,220 +1,74 @@
#!/usr/bin/env node
import { createReadStream, createWriteStream, readdirSync, statSync, unlinkSync } from 'fs';
import { join, extname } from 'path';
import { createBrotliCompress, createGzip, constants } from 'zlib';
import { pipeline } from 'stream/promises';
const DIST_DIR = './dist';
// File extensions to compress
const EXTENSIONS_TO_COMPRESS = [
'.wasm',
'.whl',
'.data',
'.zip',
'.json'
];
import fs from 'fs';
import os from 'os';
import path from 'path';
import zlib from 'zlib';
import { promisify } from 'util';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DIST_DIR = path.resolve(__dirname, '../dist');
const COMPRESSIBLE = /\.(html|js|mjs|css|json|svg|wasm|xml|txt|webmanifest)$/i;
const MIN_SIZE = 1024;
const MODES = { o: [], g: ['.gz'], b: ['.br'], all: ['.gz', '.br'] };
const mode = process.env.COMPRESSION_MODE || process.argv[2] || 'all';
const brotli = promisify(zlib.brotliCompress);
const gzip = promisify(zlib.gzip);
const ENCODERS = {
'.gz': (data) => gzip(data, { level: 9 }),
'.br': (data) =>
brotli(data, {
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 },
}),
};
const VALID_MODES = ['g', 'b', 'o', 'all'];
if (!VALID_MODES.includes(mode)) {
console.error(`❌ Invalid compression mode: ${mode}`);
console.error(` Valid modes: g (gzip only), b (brotli only), o (original only), all (all formats)`);
process.exit(1);
const mode = process.env.COMPRESSION_MODE || 'o';
if (!MODES[mode]) {
console.error(
`Invalid COMPRESSION_MODE "${mode}". Use o (none), g (.gz), b (.br) or all (.gz and .br).`
);
process.exit(1);
}
console.log(`🔧 Compression mode: ${mode}`);
console.log(` ${getModeDescription(mode)}\n`);
function getModeDescription(mode) {
const descriptions = {
g: 'gzip ONLY (keeps .gz, deletes originals and .br)',
b: 'brotli ONLY (keeps .br, deletes originals and .gz)',
o: 'original ONLY (keeps originals, deletes .gz and .br)',
all: 'ALL formats (keeps .gz, .br, and originals)'
};
return descriptions[mode];
function listFiles(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return listFiles(full);
return entry.isFile() ? [full] : [];
});
}
function getAllFiles(dir, files = []) {
const entries = readdirSync(dir);
const queue = MODES[mode].length
? listFiles(DIST_DIR).filter(
(file) => COMPRESSIBLE.test(file) && fs.statSync(file).size >= MIN_SIZE
)
: [];
let written = 0;
for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
getAllFiles(fullPath, files);
} else {
files.push(fullPath);
}
async function compress(file) {
const { mtimeMs } = fs.statSync(file);
const stale = MODES[mode].filter((ext) => {
const copy = `${file}${ext}`;
return !fs.existsSync(copy) || fs.statSync(copy).mtimeMs < mtimeMs;
});
if (!stale.length) return;
const data = await fs.promises.readFile(file);
for (const ext of stale) {
const encoded = await ENCODERS[ext](data);
if (encoded.length < data.length) {
await fs.promises.writeFile(`${file}${ext}`, encoded);
written++;
} else {
fs.rmSync(`${file}${ext}`, { force: true });
}
return files;
}
}
/**
* Compress a file with Brotli (level 11)
*/
async function compressBrotli(inputPath, outputPath) {
const brotli = createBrotliCompress({
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC,
},
});
await pipeline(
createReadStream(inputPath),
brotli,
createWriteStream(outputPath)
);
async function worker() {
for (let file = queue.pop(); file; file = queue.pop()) {
await compress(file);
}
}
/**
* Compress a file with gzip (level 9)
*/
async function compressGzip(inputPath, outputPath) {
const gzip = createGzip({ level: 9 });
await Promise.all(Array.from({ length: os.availableParallelism() }, worker));
await pipeline(
createReadStream(inputPath),
gzip,
createWriteStream(outputPath)
);
}
function deleteCompressedFiles() {
console.log('🗑️ Deleting compressed files...\n');
const allFiles = getAllFiles(DIST_DIR);
let deletedCount = 0;
for (const file of allFiles) {
if (file.endsWith('.br') || file.endsWith('.gz')) {
unlinkSync(file);
deletedCount++;
console.log(` Deleted: ${file}`);
}
}
console.log(`\n✅ Deleted ${deletedCount} compressed files\n`);
}
/**
* Delete original files (keep only compressed versions)
*/
function deleteOriginalFiles(filePaths, extension) {
console.log(`🗑️ Deleting original files (keeping only ${extension})...\n`);
let deletedCount = 0;
for (const file of filePaths) {
try {
unlinkSync(file);
deletedCount++;
console.log(` Deleted: ${file}`);
} catch (error) {
console.error(` ⚠️ Failed to delete ${file}: ${error.message}`);
}
}
console.log(`\n✅ Deleted ${deletedCount} original files\n`);
}
/**
* Delete specific compressed format
*/
function deleteCompressedFormat(extension) {
console.log(`🗑️ Deleting ${extension} files...\n`);
const allFiles = getAllFiles(DIST_DIR);
let deletedCount = 0;
for (const file of allFiles) {
if (file.endsWith(extension)) {
unlinkSync(file);
deletedCount++;
console.log(` Deleted: ${file}`);
}
}
console.log(`\n✅ Deleted ${deletedCount} ${extension} files\n`);
}
async function compressStaticAssets() {
if (mode === 'o') {
deleteCompressedFiles();
return;
}
console.log('🔍 Scanning for static assets to compress...\n');
const allFiles = getAllFiles(DIST_DIR);
const filesToCompress = allFiles.filter(file => {
const ext = extname(file);
const stat = statSync(file);
if (file.endsWith('.br') || file.endsWith('.gz')) {
return false;
}
return EXTENSIONS_TO_COMPRESS.includes(ext) && stat.size >= MIN_SIZE;
});
console.log(`📦 Found ${filesToCompress.length} files to compress\n`);
let processedCount = 0;
const shouldCompressBrotli = mode === 'b' || mode === 'all';
const shouldCompressGzip = mode === 'g' || mode === 'all';
for (const file of filesToCompress) {
const stat = statSync(file);
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
try {
let brotliStat, gzipStat;
if (shouldCompressBrotli) {
await compressBrotli(file, `${file}.br`);
brotliStat = statSync(`${file}.br`);
}
if (shouldCompressGzip) {
await compressGzip(file, `${file}.gz`);
gzipStat = statSync(`${file}.gz`);
}
processedCount++;
console.log(`✅ ${file}`);
console.log(` Original: ${sizeMB} MB`);
if (brotliStat) {
const brotliSizeMB = (brotliStat.size / (1024 * 1024)).toFixed(2);
console.log(` Brotli: ${brotliSizeMB} MB (${((brotliStat.size / stat.size) * 100).toFixed(1)}%)`);
}
if (gzipStat) {
const gzipSizeMB = (gzipStat.size / (1024 * 1024)).toFixed(2);
console.log(` Gzip: ${gzipSizeMB} MB (${((gzipStat.size / stat.size) * 100).toFixed(1)}%)`);
}
console.log('');
} catch (error) {
console.error(`❌ Failed to compress ${file}:`, error.message);
}
}
console.log(`\n🎉 Compressed ${processedCount}/${filesToCompress.length} files successfully!\n`);
if (mode === 'g') {
deleteCompressedFormat('.br');
deleteOriginalFiles(filesToCompress, '.gz');
} else if (mode === 'b') {
deleteCompressedFormat('.gz');
deleteOriginalFiles(filesToCompress, '.br');
}
}
compressStaticAssets().catch(err => {
console.error('❌ Compression failed:', err);
process.exit(1);
});
console.log(`Compression mode ${mode}: wrote ${written} precompressed files.`);
+1 -1
View File
@@ -181,7 +181,7 @@ OPTIONS:
--dockerfile <path> Dockerfile to use (default: Dockerfile)
--simple-mode Enable Simple Mode
--base-url <path> Subdirectory base URL (e.g. /pdf/)
--compression <mode> Compression: g, b, o, all (default: all)
--compression <mode> Compression: g, b, o, all (default: o)
--language <code> Default UI language (e.g. fr, de, es)
--brand-name <name> Custom brand name
--brand-logo <path> Logo path relative to public/
-36
View File
@@ -1,36 +0,0 @@
import fs from 'fs';
import path from 'path';
import zlib from 'zlib';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DIST_DIR = path.resolve(__dirname, '../dist');
function listHtml(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return listHtml(full);
return entry.name.endsWith('.html') ? [full] : [];
});
}
let refreshed = 0;
for (const file of listHtml(DIST_DIR)) {
const brotli = `${file}.br`;
const gzip = `${file}.gz`;
if (!fs.existsSync(brotli) && !fs.existsSync(gzip)) continue;
const html = fs.readFileSync(file);
if (fs.existsSync(brotli)) {
fs.writeFileSync(
brotli,
zlib.brotliCompressSync(html, {
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 },
})
);
}
if (fs.existsSync(gzip)) {
fs.writeFileSync(gzip, zlib.gzipSync(html, { level: 9 }));
}
refreshed++;
}
console.log(`Recompressed ${refreshed} precompressed HTML files.`);