Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/precompress-web-assets.mjs
T

123 lines
4.4 KiB
JavaScript

import { constants, brotliCompress as brotliCompressCallback, gzip as gzipCallback } from "node:zlib";
import { promisify } from "node:util";
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
const brotliCompress = promisify(brotliCompressCallback);
const gzip = promisify(gzipCallback);
const COMPRESSIBLE_EXTENSIONS = new Set([".css", ".html", ".js", ".json", ".mjs", ".svg", ".txt", ".webmanifest", ".xml"]);
const MINIMUM_INPUT_BYTES = 1_024;
const MAX_COMPRESSION_WORKERS = 4;
export async function precompressWebAssets(rootDirectory) {
const files = await collectCompressibleFiles(rootDirectory);
let nextIndex = 0;
const report = { considered: files.length, brotliWritten: 0, gzipWritten: 0 };
await Promise.all(
Array.from({ length: Math.min(MAX_COMPRESSION_WORKERS, files.length) }, async () => {
while (nextIndex < files.length) {
const filePath = files[nextIndex++];
const input = await readFile(filePath);
const [brotli, gzipped] = await Promise.all([
brotliCompress(input, {
params: {
[constants.BROTLI_PARAM_QUALITY]: 7,
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
},
}),
gzip(input, { level: 9 }),
]);
if (brotli.length < input.length) {
await writeFile(`${filePath}.br`, brotli);
report.brotliWritten += 1;
}
if (gzipped.length < input.length) {
await writeFile(`${filePath}.gz`, gzipped);
report.gzipWritten += 1;
}
}
})
);
return report;
}
/**
* React Router can emit an unreferenced `server-build-*.css` copy of the root
* stylesheet into the public client directory. Keeping and precompressing the
* duplicate adds image bytes and build CPU without serving a request. Delete
* it only when another byte-identical stylesheet exists and no emitted JS,
* HTML, or JSON asset references its filename.
*/
export async function pruneUnreferencedDuplicateCss(rootDirectory) {
let entries;
try {
entries = await readdir(rootDirectory, { withFileTypes: true });
} catch (error) {
if (error?.code === "ENOENT") return { removed: 0, bytesRemoved: 0 };
throw error;
}
const cssEntries = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".css"));
const candidates = cssEntries.filter((entry) => entry.name.startsWith("server-build-"));
if (candidates.length === 0) return { removed: 0, bytesRemoved: 0 };
const referenceEntries = entries.filter(
(entry) => entry.isFile() && [".html", ".js", ".json", ".mjs"].includes(path.extname(entry.name).toLowerCase()),
);
const referenceSources = await Promise.all(referenceEntries.map((entry) => readFile(path.join(rootDirectory, entry.name), "utf8")));
const cssContents = new Map(
await Promise.all(cssEntries.map(async (entry) => [entry.name, await readFile(path.join(rootDirectory, entry.name))])),
);
let removed = 0;
let bytesRemoved = 0;
for (const candidate of candidates) {
if (referenceSources.some((source) => source.includes(candidate.name))) continue;
const candidateContents = cssContents.get(candidate.name);
const duplicateExists = cssEntries.some(
(entry) => entry.name !== candidate.name && candidateContents?.equals(cssContents.get(entry.name)),
);
if (!duplicateExists || !candidateContents) continue;
await unlink(path.join(rootDirectory, candidate.name));
removed += 1;
bytesRemoved += candidateContents.length;
}
return { removed, bytesRemoved };
}
async function collectCompressibleFiles(rootDirectory) {
const files = [];
await visit(rootDirectory, files);
return files;
}
async function visit(directory, files) {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error) {
if (error?.code === "ENOENT") return;
throw error;
}
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(fullPath, files);
} else if (
entry.isFile() &&
!entry.name.endsWith(".br") &&
!entry.name.endsWith(".gz") &&
COMPRESSIBLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())
) {
const fileStat = await stat(fullPath);
if (fileStat.size >= MINIMUM_INPUT_BYTES) {
files.push(fullPath);
}
}
}
}