feat: minification serveur à la volée (terser + fallback Bun) et cache immutable
- 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
This commit is contained in:
parent
5b4b2caded
commit
e0811492fa
3 changed files with 79 additions and 11 deletions
23
bun.lock
23
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=="],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"dependencies": {
|
||||
"marked": "^18.0.9",
|
||||
"sharp": "^0.35.3",
|
||||
"terser": "^5.51.2",
|
||||
"wordpress-honeypot": "^1.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, { hash: string; data: Buffer }>()
|
||||
|
||||
async function minifySrc(src: string, ext: string): Promise<string> {
|
||||
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<string, CachedFile>()
|
||||
|
||||
type ArticleLang = { title: string; ogTitle: string; description: string; body: string }
|
||||
|
|
@ -1059,27 +1080,40 @@ const types: Record<string, string> = {
|
|||
".pdf": "application/pdf",
|
||||
}
|
||||
|
||||
function buildFileCache() {
|
||||
async function buildFileCache() {
|
||||
const next = new Map<string, CachedFile>()
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue