From e0811492fac387528024a07d7902ffcac80b8fde Mon Sep 17 00:00:00 2001 From: riricdev Date: Sun, 6 Sep 2026 14:48:11 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20minification=20serveur=20=C3=A0=20la=20?= =?UTF-8?q?vol=C3=A9e=20(terser=20+=20fallback=20Bun)=20et=20cache=20immut?= =?UTF-8?q?able?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - import terser/Bun.Transpiler (fallback si terser indisponible) - minifySrc(): CSS via regex, JS via terser.minify (compress+mangle) sinon source - buildFileCache asynchrone, etag stable sha1(content), cache de minification hashé - Cache-Control: html no-store, assets ?v= immutable 1 an, sinon must-revalidate --- bun.lock | 23 ++++++++++++++++++ package.json | 1 + src/server.ts | 66 ++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index d6ab8a2..8dfeb10 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "marked": "^18.0.9", "sharp": "^0.35.3", + "terser": "^5.51.2", "wordpress-honeypot": "^1.1.2", }, "devDependencies": { @@ -74,12 +75,28 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "marked": ["marked@18.0.9", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w=="], @@ -88,6 +105,12 @@ "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "terser": ["terser@5.51.2", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/package.json b/package.json index 5ce96c4..f99f310 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "marked": "^18.0.9", "sharp": "^0.35.3", + "terser": "^5.51.2", "wordpress-honeypot": "^1.1.2" } } diff --git a/src/server.ts b/src/server.ts index a2333f3..76dcc34 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,11 +15,32 @@ import { docsPage } from "./docs" import { zmapWsHandler, zmapWsMessage, zmapWsClose, zmapWsKeepAlive } from "./zmap-ws" import { join, normalize } from "node:path" import { statSync, existsSync, readFileSync, writeFileSync, chmodSync, readdirSync } from "node:fs" -import { randomBytes } from "node:crypto" +import { randomBytes, createHash } from "node:crypto" import { spawn, execSync } from "node:child_process" import os from "node:os" type CachedFile = { data: Buffer; type: string; etag: string } + +let terser: typeof import("terser") | null = null +try { terser = await import("terser") } catch {} + +let trans: import("bun").Transpiler | null = null +try { trans = new Bun.Transpiler({ loader: "js", minify: { whitespace: true, syntax: true, semicolons: true } } as never) } catch {} + +const MINIFY_EXT = new Set([".js", ".mjs", ".css"]) +const minifyCache = new Map() + +async function minifySrc(src: string, ext: string): Promise { + try { + if (ext === ".css") { + return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{};:,])\s*/g, "$1").replace(/;}/g, "}").trim() + } + if (terser) return (await terser.minify(src, { compress: true, mangle: true, format: { comments: false } })).code || src + if (trans) return trans.transformSync(src) + } catch { /* en cas d'échec on garde la source lisible */ } + return src +} + let fileCache = new Map() type ArticleLang = { title: string; ogTitle: string; description: string; body: string } @@ -1059,27 +1080,40 @@ const types: Record = { ".pdf": "application/pdf", } -function buildFileCache() { +async function buildFileCache() { const next = new Map() - function walk(dir: string) { + async function walk(dir: string) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name) - if (entry.isDirectory()) { walk(full); continue } + if (entry.isDirectory()) { await walk(full); continue } try { const data = readFileSync(full) as Buffer const ext = full.slice(full.lastIndexOf(".")).toLowerCase() const rel = full.slice(root.length) + const hash = createHash("sha1").update(data).digest("hex") + let out = data + if (MINIFY_EXT.has(ext)) { + const cached = minifyCache.get(rel) + if (cached && cached.hash === hash) { + out = cached.data + } else { + const minified = await minifySrc(data.toString("utf8"), ext) + out = Buffer.from(minified) + minifyCache.set(rel, { hash, data: out }) + } + } next.set(rel, { - data, + data: out, type: types[ext] ?? "application/octet-stream", - etag: `"${data.length}-${Date.now()}"`, + etag: `"${hash}"`, }) } catch {} } } - walk(root) + await walk(root) fileCache = next - console.log(`[cache] ${fileCache.size} fichiers en RAM (${(process.memoryUsage().rss / 1048576).toFixed(0)} Mo RSS)`) + const saved = process.memoryUsage().rss / 1048576 + console.log(`[cache] ${fileCache.size} fichiers en RAM (${saved.toFixed(0)} Mo RSS)`) } function startWatcher() { @@ -1108,7 +1142,7 @@ function startWatcher() { } console.log(`Zektyc sert ${root} sur http://${host}:${port}`) -buildFileCache() +await buildFileCache() startWatcher() writeSitemaps(readArticles()) @@ -1201,10 +1235,15 @@ server = serve({ const ifNoneMatch = req.headers.get("if-none-match") if (ifNoneMatch && ifNoneMatch === cached.etag) return new Response(null, { status: 304 }) const isHtml = cached.type.startsWith("text/html") + const cacheControl = isHtml + ? "no-store" + : url.searchParams.has("v") + ? "public, max-age=31536000, immutable" + : "public, max-age=300, must-revalidate" return new Response(cached.data, { headers: { "Content-Type": cached.type, - "Cache-Control": isHtml ? "no-store" : "public, max-age=300", + "Cache-Control": cacheControl, "ETag": cached.etag, }, }) @@ -1213,10 +1252,15 @@ server = serve({ const f = file(filePath) if (await f.exists()) { const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase() + const cacheControl = ext === ".html" + ? "no-cache" + : url.searchParams.has("v") + ? "public, max-age=31536000, immutable" + : "public, max-age=300, must-revalidate" return new Response(f, { headers: { "Content-Type": types[ext] ?? "application/octet-stream", - "Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=300", + "Cache-Control": cacheControl, }, }) }